diff --git a/consumer-config.yaml b/consumer-config.yaml
new file mode 100644
index 0000000..7c1c7a2
--- /dev/null
+++ b/consumer-config.yaml
@@ -0,0 +1,20 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: consumer-config
+data:
+ application.yml: |
+ spring:
+ pulsar:
+ client: # see here for all configurations: https://pulsar.apache.org/reference/#/4.0.x/client/client-configuration-client
+ clientConfig:
+ serviceUrl: "pulsar://host.docker.internal:6650"
+ consumer:
+ enabled: true
+ consumerConfig: # see here for all configurations: https://pulsar.apache.org/reference/#/4.0.x/client/client-configuration-producer
+ topicNames: "tester-tester"
+ admin:
+ adminConfig: # Accepts the same key-value pair configurations as pulsar client: https://pulsar.apache.org/reference/#/4.0.x/client/client-configuration-client
+ serviceUrl: "http://host.docker.internal:8080"
+
+
\ No newline at end of file
diff --git a/consumer-pipeline.yaml b/consumer-pipeline.yaml
new file mode 100644
index 0000000..69c6c3b
--- /dev/null
+++ b/consumer-pipeline.yaml
@@ -0,0 +1,43 @@
+apiVersion: numaflow.numaproj.io/v1alpha1
+kind: Pipeline
+metadata:
+ name: consumer-pipeline
+spec:
+ limits:
+ readBatchSize: 1 # Change if you want a different batch size
+ vertices:
+ - name: in
+ scale:
+ min: 1
+ volumes:
+ - name: pulsar-config-volume
+ configMap:
+ name: consumer-config
+ items:
+ - key: application.yml
+ path: application.yml
+ source:
+ udsource:
+ container:
+ image: apache-pulsar-java:v0.3.0
+ args: [ "--spring.config.location=file:/conf/application.yml" ]
+ imagePullPolicy: Never
+ volumeMounts:
+ - name: pulsar-config-volume
+ mountPath: /conf
+ - name: p1
+ scale:
+ min: 1
+ udf:
+ builtin:
+ name: cat
+ - name: out
+ scale:
+ min: 1
+ sink:
+ log: {}
+ edges:
+ - from: in
+ to: p1
+ - from: p1
+ to: out
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index cdaa34b..ba3d2c4 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,4 +1,3 @@
-version: '3.5'
services:
pulsar:
diff --git a/pom.xml b/pom.xml
index 35e19df..97c9b0d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -35,6 +35,16 @@
spring-boot-starter-test
test
+
+ org.apache.avro
+ avro
+ 1.11.0
+
+
+ org.json
+ json
+ 20231013
+
diff --git a/producer-config.yaml b/producer-config.yaml
new file mode 100644
index 0000000..26eadfd
--- /dev/null
+++ b/producer-config.yaml
@@ -0,0 +1,18 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: producer-config
+data:
+ application.yml: |
+ spring:
+ pulsar:
+ client: # see here for all configurations: https://pulsar.apache.org/reference/#/4.0.x/client/client-configuration-client
+ clientConfig:
+ serviceUrl: "pulsar://host.docker.internal:6650"
+ producer:
+ enabled: true
+ producerConfig: # see here for all configurations: https://pulsar.apache.org/reference/#/4.0.x/client/client-configuration-producer
+ topicName: "tester-tester"
+ sendTimeoutMs: 2000
+
+
\ No newline at end of file
diff --git a/producer-pipeline.yaml b/producer-pipeline.yaml
new file mode 100644
index 0000000..7901727
--- /dev/null
+++ b/producer-pipeline.yaml
@@ -0,0 +1,44 @@
+apiVersion: numaflow.numaproj.io/v1alpha1
+kind: Pipeline
+metadata:
+ name: producer-pipeline
+spec:
+ vertices:
+ - name: in
+ scale:
+ min: 1
+ source:
+ generator:
+ rpu: 1
+ duration: 1s
+ msgSize: 10
+ - name: p1
+ scale:
+ min: 1
+ udf:
+ builtin:
+ name: cat
+ - name: out
+ scale:
+ min: 1
+ volumes: # Shared between containers that are part of the same pod, useful for sharing configurations
+ - name: pulsar-config-volume
+ configMap:
+ name: producer-config
+ items:
+ - key: application.yml
+ path: application.yml
+ sink:
+ udsink:
+ container:
+ image: apache-pulsar-java:v0.3.0 # TO DO: Replace with quay.io link
+ args: [ "--spring.config.location=file:/conf/application.yml" ] # Use external configuration file
+ imagePullPolicy: Never
+ volumeMounts:
+ - name: pulsar-config-volume
+ mountPath: /conf
+ edges:
+ - from: in
+ to: p1
+ - from: p1
+ to: out
\ No newline at end of file
diff --git a/src/main/java/io/numaproj/pulsar/config/producer/PulsarProducerConfig.java b/src/main/java/io/numaproj/pulsar/config/producer/PulsarProducerConfig.java
index d92d96f..0c2ef0f 100644
--- a/src/main/java/io/numaproj/pulsar/config/producer/PulsarProducerConfig.java
+++ b/src/main/java/io/numaproj/pulsar/config/producer/PulsarProducerConfig.java
@@ -8,35 +8,51 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
+import org.springframework.core.io.ClassPathResource;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.UUID;
+import java.util.Optional;
@Slf4j
@Configuration
public class PulsarProducerConfig {
- @Autowired
- private Environment env;
-
- @Bean
- @ConditionalOnProperty(prefix = "spring.pulsar.producer", name = "enabled", havingValue = "true", matchIfMissing = false)
- public Producer pulsarProducer(PulsarClient pulsarClient, PulsarProducerProperties pulsarProducerProperties)
- throws Exception {
- String podName = env.getProperty("NUMAFLOW_POD", "pod-" + UUID.randomUUID());
- String producerName = "producerName";
-
- Map producerConfig = pulsarProducerProperties.getProducerConfig();
- if (producerConfig.containsKey(producerName)) {
- log.warn("User configured a 'producerName' in the config, but this can cause errors if multiple pods spin "
- + "up with the same name. Overriding with '{}'", podName);
+ @Autowired
+ private Environment env;
+
+ @Bean
+ @ConditionalOnProperty(prefix = "spring.pulsar.producer", name = "enabled", havingValue = "true", matchIfMissing = false)
+ public Producer pulsarProducer(PulsarClient pulsarClient,
+ PulsarProducerProperties pulsarProducerProperties)
+ throws Exception {
+ String podName = env.getProperty("NUMAFLOW_POD", "pod-" + UUID.randomUUID());
+ String producerName = "producerName";
+
+ Map producerConfig = pulsarProducerProperties.getProducerConfig();
+ if (producerConfig.containsKey(producerName)) {
+ log.warn("User configured a 'producerName' in the config, but this can cause errors if multiple pods spin "
+ + "up with the same name. Overriding with '{}'", podName);
+ }
+ producerConfig.put(producerName, podName);
+
+ // Optionally load schema for client-side validation if schema file exists
+ try {
+ String schemaStr = new String(
+ new ClassPathResource("schema.avsc").getInputStream().readAllBytes());
+ org.apache.avro.Schema avroSchema = new org.apache.avro.Schema.Parser().parse(schemaStr);
+ log.info("Found AVRO schema for client-side validation: {}", avroSchema.toString(true));
+ pulsarProducerProperties.setAvroSchema(Optional.of(avroSchema));
+ } catch (Exception e) {
+ log.info("No schema.avsc found or error loading schema. Client-side validation will be disabled.");
+ pulsarProducerProperties.setAvroSchema(Optional.empty());
+ }
+
+ // Create producer with byte[] schema for maximum flexibility
+ return pulsarClient.newProducer(Schema.BYTES)
+ .loadConf(producerConfig)
+ .create();
}
- producerConfig.put(producerName, podName);
-
- return pulsarClient.newProducer(Schema.BYTES)
- .loadConf(producerConfig)
- .create();
- }
}
\ No newline at end of file
diff --git a/src/main/java/io/numaproj/pulsar/config/producer/PulsarProducerProperties.java b/src/main/java/io/numaproj/pulsar/config/producer/PulsarProducerProperties.java
index c8bc25c..d36fc4d 100644
--- a/src/main/java/io/numaproj/pulsar/config/producer/PulsarProducerProperties.java
+++ b/src/main/java/io/numaproj/pulsar/config/producer/PulsarProducerProperties.java
@@ -2,12 +2,13 @@
import lombok.Getter;
import lombok.Setter;
-
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
+import org.apache.avro.Schema;
import java.util.HashMap;
import java.util.Map;
+import java.util.Optional;
@Getter
@Setter
@@ -15,4 +16,5 @@
@ConfigurationProperties(prefix = "spring.pulsar.producer")
public class PulsarProducerProperties {
private Map producerConfig = new HashMap<>(); // Default to an empty map
+ private Optional avroSchema = Optional.empty(); // Optional schema for client-side validation
}
diff --git a/src/main/java/io/numaproj/pulsar/consumer/PulsarSource.java b/src/main/java/io/numaproj/pulsar/consumer/PulsarSource.java
index f0872fb..cde7032 100644
--- a/src/main/java/io/numaproj/pulsar/consumer/PulsarSource.java
+++ b/src/main/java/io/numaproj/pulsar/consumer/PulsarSource.java
@@ -22,6 +22,7 @@
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
+import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
@@ -30,6 +31,16 @@
import java.util.Map;
import java.util.Set;
+import org.apache.avro.Schema;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.avro.generic.GenericDatumReader;
+import org.apache.avro.io.DatumReader;
+import org.apache.avro.io.DecoderFactory;
+import org.apache.avro.io.BinaryDecoder;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.core.io.ClassPathResource;
+import org.json.JSONObject;
+
@Slf4j
@Component
@ConditionalOnProperty(prefix = "spring.pulsar.consumer", name = "enabled", havingValue = "true")
@@ -49,13 +60,108 @@ public class PulsarSource extends Sourcer {
@Autowired
PulsarConsumerProperties pulsarConsumerProperties;
+ private Schema avroSchema;
+
@PostConstruct
public void startServer() throws Exception {
+ // Load the Avro schema
+ try {
+ ObjectMapper mapper = new ObjectMapper();
+ String schemaStr = new String(new ClassPathResource("schema.avsc").getInputStream().readAllBytes());
+ avroSchema = new Schema.Parser().parse(schemaStr);
+ log.info("Loaded AVRO schema for consumer: {}", avroSchema.toString(true));
+ } catch (IOException e) {
+ log.error("Failed to parse AVRO schema", e);
+ throw e;
+ }
+
server = new Server(this);
server.start();
server.awaitTermination();
}
+ private GenericRecord deserializeAvroRecord(byte[] bytes) throws IOException {
+ DatumReader reader = new GenericDatumReader<>(avroSchema);
+ BinaryDecoder decoder = DecoderFactory.get().binaryDecoder(bytes, null);
+ return reader.read(null, decoder);
+ }
+
+ private void logAvroRecord(GenericRecord record) {
+ log.info(" Createdts: {}", record.get("Createdts"));
+ if (record.get("Data") != null) {
+ GenericRecord dataRecord = (GenericRecord) record.get("Data");
+ log.info(" Data:");
+ log.info(" value: {}", dataRecord.get("value"));
+ log.info(" padding: {}", dataRecord.get("padding"));
+ } else {
+ log.info(" Data: null");
+ }
+ }
+
+ private byte[] convertAvroRecordToJson(GenericRecord record) {
+ JSONObject json = convertAvroFieldToJson(record);
+ return json.toString().getBytes(StandardCharsets.UTF_8);
+ }
+
+ private JSONObject convertAvroFieldToJson(Object value) {
+ if (value == null) {
+ return null;
+ }
+
+ if (value instanceof GenericRecord) {
+ GenericRecord record = (GenericRecord) value;
+ JSONObject json = new JSONObject();
+ record.getSchema().getFields().forEach(field -> {
+ String fieldName = field.name();
+ Object fieldValue = record.get(fieldName);
+ if (fieldValue instanceof GenericRecord) {
+ json.put(fieldName, convertAvroFieldToJson(fieldValue));
+ } else if (fieldValue instanceof List) {
+ json.put(fieldName, convertAvroListToJson((List>) fieldValue));
+ } else if (fieldValue instanceof Map) {
+ json.put(fieldName, convertAvroMapToJson((Map, ?>) fieldValue));
+ } else {
+ json.put(fieldName, fieldValue);
+ }
+ });
+ return json;
+ }
+
+ return new JSONObject().put("value", value);
+ }
+
+ private JSONObject convertAvroMapToJson(Map, ?> map) {
+ JSONObject json = new JSONObject();
+ map.forEach((key, value) -> {
+ if (value instanceof GenericRecord) {
+ json.put(key.toString(), convertAvroFieldToJson(value));
+ } else if (value instanceof List) {
+ json.put(key.toString(), convertAvroListToJson((List>) value));
+ } else if (value instanceof Map) {
+ json.put(key.toString(), convertAvroMapToJson((Map, ?>) value));
+ } else {
+ json.put(key.toString(), value);
+ }
+ });
+ return json;
+ }
+
+ private org.json.JSONArray convertAvroListToJson(List> list) {
+ org.json.JSONArray jsonArray = new org.json.JSONArray();
+ list.forEach(item -> {
+ if (item instanceof GenericRecord) {
+ jsonArray.put(convertAvroFieldToJson(item));
+ } else if (item instanceof List) {
+ jsonArray.put(convertAvroListToJson((List>) item));
+ } else if (item instanceof Map) {
+ jsonArray.put(convertAvroMapToJson((Map, ?>) item));
+ } else {
+ jsonArray.put(item);
+ }
+ });
+ return jsonArray;
+ }
+
@Override
public void read(ReadRequest request, OutputObserver observer) {
// If there are messages not acknowledged, return
@@ -77,19 +183,30 @@ public void read(ReadRequest request, OutputObserver observer) {
return;
}
- // Process each message in the batch.
for (org.apache.pulsar.client.api.Message pMsg : batchMessages) {
String msgId = pMsg.getMessageId().toString();
- log.info("Consumed Pulsar message [id: {}]: {}", pMsg.getMessageId(),
- new String(pMsg.getValue(), StandardCharsets.UTF_8));
+ byte[] rawBytes = pMsg.getValue();
- byte[] offsetBytes = msgId.getBytes(StandardCharsets.UTF_8);
- Offset offset = new Offset(offsetBytes);
+ try {
+ GenericRecord deserializedRecord = deserializeAvroRecord(rawBytes);
+ log.info("Consumed Pulsar message [id: {}]:", msgId);
+ logAvroRecord(deserializedRecord);
+
+ byte[] jsonBytes = convertAvroRecordToJson(deserializedRecord);
+ log.debug("Converted to JSON: {}", new String(jsonBytes, StandardCharsets.UTF_8));
- Message message = new Message(pMsg.getValue(), offset, Instant.now());
- observer.send(message);
+ byte[] offsetBytes = msgId.getBytes(StandardCharsets.UTF_8);
+ Offset offset = new Offset(offsetBytes);
- messagesToAck.put(msgId, pMsg);
+ // Send the JSON bytes instead of raw Avro bytes
+ Message message = new Message(jsonBytes, offset, Instant.now());
+ observer.send(message);
+
+ messagesToAck.put(msgId, pMsg);
+ } catch (IOException e) {
+ log.error("Failed to process Avro message [id: {}]: {}", msgId, e.getMessage());
+ continue;
+ }
}
} catch (PulsarClientException e) {
log.error("Failed to get consumer or receive messages from Pulsar", e);
@@ -212,4 +329,4 @@ public List getPartitions() {
}
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/io/numaproj/pulsar/producer/DataRecord.java b/src/main/java/io/numaproj/pulsar/producer/DataRecord.java
new file mode 100644
index 0000000..bc2c811
--- /dev/null
+++ b/src/main/java/io/numaproj/pulsar/producer/DataRecord.java
@@ -0,0 +1,17 @@
+package io.numaproj.pulsar.producer;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.AllArgsConstructor;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class DataRecord {
+ @JsonProperty("value")
+ private long value;
+
+ @JsonProperty("padding")
+ private String padding;
+}
\ No newline at end of file
diff --git a/src/main/java/io/numaproj/pulsar/producer/PulsarSink.java b/src/main/java/io/numaproj/pulsar/producer/PulsarSink.java
index 0fe7f51..2472369 100644
--- a/src/main/java/io/numaproj/pulsar/producer/PulsarSink.java
+++ b/src/main/java/io/numaproj/pulsar/producer/PulsarSink.java
@@ -13,12 +13,23 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
+import org.apache.avro.Schema;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.avro.generic.GenericDatumWriter;
+import org.apache.avro.io.DatumWriter;
+import org.apache.avro.io.EncoderFactory;
+import org.apache.avro.io.BinaryEncoder;
+import io.numaproj.pulsar.config.producer.PulsarProducerProperties;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
+import java.io.IOException;
+import java.io.ByteArrayOutputStream;
+import org.json.JSONObject;
@Slf4j
@Component
@@ -31,56 +42,122 @@ public class PulsarSink extends Sinker {
@Autowired
private PulsarClient pulsarClient;
+ @Autowired
+ private PulsarProducerProperties producerProperties;
+
private Server server;
- @PostConstruct // starts server automatically when the spring context initializes
+ @PostConstruct
public void startServer() throws Exception {
server = new Server(this);
server.start();
server.awaitTermination();
}
+ private byte[] validateAndSerializeMessage(byte[] jsonBytes) throws IOException {
+ // If no schema validation is required, return the raw bytes
+ if (!producerProperties.getAvroSchema().isPresent()) {
+ return jsonBytes;
+ }
+
+ Schema avroSchema = producerProperties.getAvroSchema().get();
+
+ // Parse JSON and create Avro record
+ String jsonStr = new String(jsonBytes);
+ JSONObject json = new JSONObject(jsonStr);
+ GenericRecord record = new GenericData.Record(avroSchema);
+
+ // Set fields from JSON to record
+ avroSchema.getFields().forEach(field -> {
+ String fieldName = field.name();
+ if (json.has(fieldName)) {
+ Object value = json.get(fieldName);
+ if (value instanceof JSONObject) {
+ // Handle nested JSON object by creating a nested GenericRecord
+ JSONObject nestedJson = (JSONObject) value;
+ Schema fieldSchema = field.schema();
+ // If it's a union type, find the record type
+ if (fieldSchema.getType() == Schema.Type.UNION) {
+ for (Schema s : fieldSchema.getTypes()) {
+ if (s.getType() == Schema.Type.RECORD) {
+ fieldSchema = s;
+ break;
+ }
+ }
+ }
+ GenericRecord nestedRecord = new GenericData.Record(fieldSchema);
+ fieldSchema.getFields().forEach(nestedField -> {
+ String nestedFieldName = nestedField.name();
+ if (nestedJson.has(nestedFieldName)) {
+ nestedRecord.put(nestedFieldName, nestedJson.get(nestedFieldName));
+ }
+ });
+ record.put(fieldName, nestedRecord);
+ } else {
+ record.put(fieldName, value);
+ }
+ }
+ });
+
+ // Let Avro's built-in validation handle all the type checking and constraints
+ if (!GenericData.get().validate(avroSchema, record)) {
+ throw new IOException("Message failed Avro schema validation");
+ }
+
+ // If validation passes, serialize to Avro binary format
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(outputStream, null);
+ DatumWriter writer = new GenericDatumWriter<>(avroSchema);
+ writer.write(record, encoder);
+ encoder.flush();
+ return outputStream.toByteArray();
+ }
+
@Override
public ResponseList processMessages(DatumIterator datumIterator) {
- ResponseList.ResponseListBuilder responseListBuilder = ResponseList.newBuilder();
-
- List> futures = new ArrayList<>();
- while (true) {
- Datum datum;
- try {
- datum = datumIterator.next();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- continue;
- }
- // null means the iterator is closed, so we break
- if (datum == null) {
- break;
- }
+ List responses = new ArrayList<>();
+ List> futures = new ArrayList<>();
- final byte[] msg = datum.getValue();
- final String msgId = datum.getId();
-
- // Won't wait for broker to confirm receipt of msg before continuing
- // sendSync returns CompletableFuture which will complete when broker ack
- CompletableFuture future = producer.sendAsync(msg)
- .thenAccept(messageId -> {
- log.info("Processed message ID: {}, Content: {}", msgId, new String(msg));
- responseListBuilder.addResponse(Response.responseOK(msgId));
- })
- .exceptionally(ex -> {
- log.error("Error processing message ID {}: {}", msgId, ex.getMessage(), ex);
- responseListBuilder.addResponse(Response.responseFailure(msgId, ex.getMessage()));
- return null;
- });
+ try {
+ while (true) {
+ Datum datum = datumIterator.next();
+ if (datum == null) {
+ break;
+ }
+ try {
+ log.info("Preparing to send message: {}", new String(datum.getValue()));
+ byte[] messageBytes = validateAndSerializeMessage(datum.getValue());
+
+ CompletableFuture future = producer.sendAsync(messageBytes)
+ .thenApply(msgId -> {
+ log.info("Successfully sent message with ID: {}", msgId);
+ return Response.responseOK(datum.getId());
+ })
+ .exceptionally(throwable -> {
+ log.error("Failed to send message", throwable);
+ return Response.responseFailure(datum.getId(), throwable.getMessage());
+ });
- futures.add(future);
+ futures.add(future);
+ } catch (Exception e) {
+ log.error("Error processing message", e);
+ responses.add(Response.responseFailure(datum.getId(), e.getMessage()));
+ }
+ }
+ } catch (InterruptedException e) {
+ log.error("Iterator was interrupted", e);
+ Thread.currentThread().interrupt();
}
- // Wait for all sends to complete
+ // Wait for all async operations to complete
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
- return responseListBuilder.build();
+ // Collect all responses
+ futures.forEach(future -> responses.add(future.join()));
+
+ ResponseList.ResponseListBuilder builder = ResponseList.newBuilder();
+ responses.forEach(builder::addResponse);
+ return builder.build();
}
@PreDestroy
diff --git a/src/main/java/io/numaproj/pulsar/producer/numagen.java b/src/main/java/io/numaproj/pulsar/producer/numagen.java
new file mode 100644
index 0000000..389dd97
--- /dev/null
+++ b/src/main/java/io/numaproj/pulsar/producer/numagen.java
@@ -0,0 +1,17 @@
+package io.numaproj.pulsar.producer;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.AllArgsConstructor;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class numagen {
+ @JsonProperty("Data")
+ private DataRecord Data;
+
+ @JsonProperty("Createdts")
+ private long Createdts;
+}
\ No newline at end of file
diff --git a/src/main/resources/schema.avsc b/src/main/resources/schema.avsc
new file mode 100644
index 0000000..713b252
--- /dev/null
+++ b/src/main/resources/schema.avsc
@@ -0,0 +1,33 @@
+{
+ "type": "record",
+ "name": "numagen",
+ "fields": [
+ {
+ "name": "Createdts",
+ "type": "long"
+ },
+ {
+ "name": "Data",
+ "type": [
+ "null",
+ {
+ "type": "record",
+ "name": "DataRecord",
+ "fields": [
+ {
+ "name": "padding",
+ "type": ["null", "string"],
+ "default": null
+ },
+ {
+ "name": "value",
+ "type": "long"
+ }
+ ]
+ }
+ ],
+ "default": null
+ }
+ ],
+ "aliases": ["numagen"]
+}
diff --git a/src/main/resources/static/just-schema.json b/src/main/resources/static/just-schema.json
new file mode 100644
index 0000000..f7dfbf1
--- /dev/null
+++ b/src/main/resources/static/just-schema.json
@@ -0,0 +1,30 @@
+{
+ "fields": [
+ {
+ "name": "Data",
+ "type": {
+ "fields": [
+ {
+ "name": "value",
+ "type": "long"
+ },
+ {
+ "name": "padding",
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ ],
+ "name": "Data",
+ "type": "record"
+ }
+ },
+ {
+ "name": "Createdts",
+ "type": "long"
+ }
+ ],
+ "name": "numagen",
+ "type": "record"
+ }
diff --git a/src/test/java/io/numaproj/pulsar/config/producer/PulsarProducerConfigTest.java b/src/test/java/io/numaproj/pulsar/config/producer/PulsarProducerConfigTest.java
index b24a799..678f129 100644
--- a/src/test/java/io/numaproj/pulsar/config/producer/PulsarProducerConfigTest.java
+++ b/src/test/java/io/numaproj/pulsar/config/producer/PulsarProducerConfigTest.java
@@ -1,169 +1,169 @@
-package io.numaproj.pulsar.config.producer;
-
-import org.apache.pulsar.client.api.Producer;
-import org.apache.pulsar.client.api.ProducerBuilder;
-import org.apache.pulsar.client.api.PulsarClient;
-import org.apache.pulsar.client.api.Schema;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.core.env.Environment;
-import org.springframework.test.util.ReflectionTestUtils;
-
-import io.numaproj.pulsar.config.client.PulsarClientConfig;
-import io.numaproj.pulsar.config.client.PulsarClientProperties;
-
-import static org.junit.Assert.*;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyMap;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.*;
-
-import java.util.HashMap;
-import java.util.Map;
-
-@SpringBootTest(classes = PulsarProducerConfig.class)
-public class PulsarProducerConfigTest {
-
- private PulsarProducerConfig pulsarProducerConfig;
- private Environment mockEnvironment;
-
- // Objects used only by specific test groups
- private PulsarProducerConfig spiedConfig;
- private PulsarClient mockClient;
- private PulsarProducerProperties mockProducerProperties;
- private ProducerBuilder mockProducerBuilder;
- private Producer mockProducer;
-
- @Before
- public void setUp() throws Exception {
- pulsarProducerConfig = new PulsarProducerConfig();
- mockEnvironment = mock(Environment.class);
- ReflectionTestUtils.setField(pulsarProducerConfig, "env", mockEnvironment);
-
- mockProducerProperties = mock(PulsarProducerProperties.class);
- mockClient = mock(PulsarClient.class);
-
- spiedConfig = spy(pulsarProducerConfig);
- PulsarClientConfig mockClientConfig = mock(PulsarClientConfig.class);
- doReturn(mockClient).when(mockClientConfig).pulsarClient(any(PulsarClientProperties.class));
-
- @SuppressWarnings("unchecked")
- ProducerBuilder builder = mock(ProducerBuilder.class);
- mockProducerBuilder = builder;
-
- mockProducer = mock(Producer.class);
-
- when(mockClient.newProducer(Schema.BYTES)).thenReturn(mockProducerBuilder);
- when(mockProducerBuilder.create()).thenReturn(mockProducer);
- when(mockProducerBuilder.loadConf(anyMap())).thenReturn(mockProducerBuilder);
- }
-
- @After
- public void tearDown() {
- pulsarProducerConfig = null;
- spiedConfig = null;
- mockProducerProperties = null;
- mockClient = null;
- mockProducerBuilder = null;
- mockProducer = null;
- mockEnvironment = null;
- }
- // Test to successfully create Producer bean with valid configuration properties
- @Test
- public void pulsarProducer_validConfig() throws Exception {
- Map producerConfig = new HashMap<>();
- producerConfig.put("topicName", "test-topic");
- when(mockProducerProperties.getProducerConfig()).thenReturn(producerConfig);
-
- Producer producer = spiedConfig.pulsarProducer(mockClient, mockProducerProperties);
-
- assertNotNull("Producer should be created", producer);
-
- verify(mockProducerBuilder).loadConf(argThat(map -> "test-topic".equals(map.get("topicName"))));
- verify(mockProducerBuilder).create();
- verify(mockProducerProperties).getProducerConfig();
- }
-
- // Test which ensures an error is thrown if pulsar producer isn't created with
- // topicName
- @Test
- public void pulsarProducer_missingTopicName_throwsException() throws Exception {
- when(mockProducerProperties.getProducerConfig()).thenReturn(new HashMap<>());
-
- String expectedErrorSubstring = "Topic name must be set on the producer builder";
- when(mockProducerBuilder.create())
- .thenThrow(new IllegalArgumentException(expectedErrorSubstring));
-
- IllegalArgumentException exception = assertThrows(
- IllegalArgumentException.class,
- () -> pulsarProducerConfig.pulsarProducer(mockClient, mockProducerProperties));
-
- assertTrue(exception.getMessage().contains(expectedErrorSubstring));
- }
-
- // Test for environment variable is set, and user does NOT specify producerName
- @Test
- public void pulsarProducer_ProducerNameFromEnvVarNoUserConfig() throws Exception {
- final String envPodName = "NUMAFLOW_POD_VALUE";
- when(mockEnvironment.getProperty(eq("NUMAFLOW_POD"), anyString())).thenReturn(envPodName);
-
- Map emptyConfig = new HashMap<>();
- emptyConfig.put("topicName", "test-topic");
- when(mockProducerProperties.getProducerConfig()).thenReturn(emptyConfig);
-
- Producer producer = spiedConfig.pulsarProducer(mockClient, mockProducerProperties);
-
- assertNotNull(producer);
- // Check that the "producerName" is set to envPodName
- ArgumentCaptor