-
Notifications
You must be signed in to change notification settings - Fork 8
Support for creating Custom Event Types with schemas validation #83
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
base: main
Are you sure you want to change the base?
Changes from 24 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 |
|---|---|---|
| @@ -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` | ||
|
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. This reads very similarly to the sentence above:
How about changing the wording here to be something more like |
||
|
|
||
| ```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) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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); | ||
|
|
@@ -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); | ||
|
|
@@ -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"); | ||
|
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. This seems really fragile. So any URL that ends with 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.
Contributor
Author
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. No, not looking for custom event schema to check
Member
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. We discussed this in the CDEvents WG and agreed that extending the match to
Contributor
Author
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. 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) { | ||
|
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. nit: We can probably just this up some and inverse the if statement |
||
| 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); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.