packages feed

cloudevents-haskell (empty) → 0.1.0.0

raw patch · 11 files changed

+992/−0 lines, 11 filesdep +aesondep +autodocodecdep +autodocodec-schema

Dependencies added: aeson, autodocodec, autodocodec-schema, base, binary, binary-instances, bytestring, hw-kafka-client, iri, lens, text, time

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+## [_Unreleased_](https://github.com/freckle/cloudevents-haskell/compare/v0.1.0.0...main)++## [v0.1.0.0](https://github.com/freckle/cloudevents-haskell/tree/v0.1.0.0)++First tagged release.
+ LICENSE view
@@ -0,0 +1,21 @@+The MIT License (MIT)++Copyright (c) 2025 Renaissance Learning Inc++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,363 @@+# CloudEvents Haskell SDK++An unofficial Haskell SDK for the [CloudEvents](https://cloudevents.io/) specification, particularly for version 1.0.2 of the spec. ([JSON Schema](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/formats/cloudevents.json))++## Overview++CloudEvents is a vendor-neutral specification for defining the format of event data. This SDK provides a type-safe and idiomatic way to work with CloudEvents in Haskell, making it easier to produce and consume CloudEvents across different services and platforms.++The CloudEvents Haskell SDK allows you to:++- Create CloudEvents with strongly typed event data and extensions+- Validate CloudEvents against the specification+- Encode and decode CloudEvents in JSON format+- Integrate with message brokers like Apache Kafka++## Installation++Add the package to your project's dependencies:++### Using Cabal++```+# In your .cabal file+build-depends: cloudevents-haskell+```++### Using Stack++```yaml+# In your package.yaml file+dependencies:+  - cloudevents-haskell+```++## Key Features++- **Type-safe CloudEvent representation** - Events modeled as native Haskell types with proper validation+- **Flexible event data** - Support for arbitrary event data types via polymorphism+- **Extension mechanism** - Define and use custom extension attributes+- **Protocol bindings** - Integration with Apache Kafka (binary and structured modes)+- **JSON encoding/decoding** - Automatic serialization with Aeson via Autodocodec+- **Lenses** - Convenient access and modification of CloudEvent fields+- **Validation** - Validate CloudEvents against the JSON schema++## Quick Start Examples++### Creating a Simple CloudEvent++```haskell+import CloudEvent.V1.Event.Data+import Data.Text (Text)++-- Create a CloudEvent with Text payload and no extensions+simpleEvent :: CloudEvent EmptyExtension Text+simpleEvent =+  mkCloudEvent+    MkCloudEventArg+      { ceId = "1234-1234-1234"+      , ceSource = -- ... (Iri value)+      , ceEventType = "com.example.event.created"+      , ceDataContentType = Just "text/plain"+      , ceDataSchema = Nothing+      , ceSubject = Just "resource123"+      , ceTime = -- ... (UTCTime value)+      }+    "This is the event payload as plain text"+    EmptyExtension -- No extensions+```++### Working with Structured Data++```haskell+import CloudEvent.V1.Event.Data+import Data.Aeson (FromJSON, ToJSON)+import Data.Text (Text)+import GHC.Generics (Generic)+import Autodocodec++-- Define custom event data type+data UserCreatedEvent = UserCreatedEvent+  { userId :: Text+  , username :: Text+  , email :: Text+  }+  deriving stock (Eq, Show, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec UserCreatedEvent)++instance HasCodec UserCreatedEvent where+  codec =+    object "UserCreatedEvent" $+      UserCreatedEvent+        <$> requiredField' "userId" .= (.userId)+        <*> requiredField' "username" .= (.username)+        <*> requiredField' "email" .= (.email)++-- Create an event with structured data+userCreatedEvent :: CloudEvent EmptyExtension UserCreatedEvent+userCreatedEvent =+  mkCloudEvent+    MkCloudEventArg+      { ceId = "user-123-456"+      , ceSource = -- ... (Iri value)+      , ceEventType = "com.example.user.created"+      , ceDataContentType = Just "application/json"+      , ceDataSchema = Nothing+      , ceSubject = Just "resource123"+      , ceTime = -- ... (UTCTime value)+      }+    UserCreatedEvent+      { userId = "123"+      , username = "johndoe"+      , email = "john@example.com"+      }+    EmptyExtension+```++### Using Custom Extensions++```haskell+import CloudEvent.V1.Event.Data+import Data.Aeson (FromJSON, ToJSON)+import Data.Text (Text)+import GHC.Generics (Generic)+import Autodocodec++-- Define custom extension attributes+data TraceExtension = TraceExtension+  { traceId :: Text+  , spanId :: Text+  }+  deriving stock (Eq, Show, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec TraceExtension)++instance HasCodec TraceExtension where+  codec = object "TraceExtension" objectCodec++-- This is required+instance HasObjectCodec TraceExtension where+  objectCodec =+    TraceExtension+      <$> requiredField' "traceId" .= (.traceId)+      <*> requiredField' "spanId" .= (.spanId)++-- Create an event with custom extensions+eventWithTracing :: CloudEvent TraceExtension Text+eventWithTracing =+  mkCloudEvent+    MkCloudEventArg+      { ceId = "evt-789-abc"+      , ceSource = -- ... (Iri value)+      , ceEventType = "com.example.order.processed"+      , ceDataContentType = Just "text/plain"+      , ceDataSchema = Nothing+      , ceSubject = Just "order/456"+      , ceTime = -- ... (UTCTime value)+      }+    "Order 456 has been processed successfully"+    TraceExtension+      { traceId = "trace-123456789"+      , spanId = "span-abcdef"+      }+```++## Validating CloudEvents++The SDK provides functionality to validate CloudEvents against the specification:++```haskell+import CloudEvent.V1.Event.Data+import CloudEvent.V1.Event.Validation+import Data.Aeson (Value, toJSON)++-- Validate a CloudEvent JSON representation+validateJsonCloudEvent :: Value -> Bool+validateJsonCloudEvent jsonValue =+  isValidCloudEvent @MyEventData @MyExtensions jsonValue++-- Example usage:+-- let eventJson = toJSON myCloudEvent+-- if validateJsonCloudEvent eventJson+--   then putStrLn "Valid CloudEvent"+--   else putStrLn "Invalid CloudEvent"+```++## Kafka Integration++The SDK provides integration with Apache Kafka through the `CloudEvent.V1.Bindings.Kafka` module:++```haskell+import CloudEvent.V1.Bindings.Kafka+import CloudEvent.V1.Event.Data+import Data.Text (Text)+import Kafka.Producer (TopicName)++-- Create a CloudEvent with Kafka extension+kafkaEvent :: CloudEvent KafkaCloudEventExtAttributes Text+kafkaEvent =+  mkCloudEvent+    MkCloudEventArg+      { ceId = "evt-kafka-123"+      , ceSource = -- ... (Iri value)+      , ceEventType = "com.example.inventory.updated"+      , ceDataContentType = Just "text/plain"+      , ceDataSchema = Nothing+      , ceSubject = Just "product/123"+      , ceTime = -- ... (UTCTime value)+      }+    "Product 123 inventory updated to 42 units"+    KafkaCloudEventExtAttributes{partitionKey = "product-123"}++-- Send using binary content mode+sendBinary :: TopicName -> CloudEvent KafkaCloudEventExtAttributes Text -> ProducerRecord+sendBinary topic event = toBinaryKafkaMessage topic event+-- Use with Kafka producer: producerSend producer producerRecord++-- Send using structured content mode+sendStructured :: TopicName -> CloudEvent KafkaCloudEventExtAttributes Text -> ProducerRecord+sendStructured topic event = toStructuredKafkaMessage topic event+-- Use with Kafka producer: producerSend producer producerRecord+```++## Using Lenses++The SDK provides lenses for convenient access and modification of CloudEvent fields:++```haskell+import CloudEvent.V1.Event.Data+import CloudEvent.V1.Event.Lens+import Control.Lens+import Data.Text (Text)++-- Access fields using lenses+getEventId :: CloudEvent ext eventData -> Text+getEventId event = event ^. ceId++-- Modify a CloudEvent using lenses+updateSubject :: Text -> CloudEvent ext eventData -> CloudEvent ext eventData+updateSubject newSubject event =+  event & ceSubject .~ Just newSubject+```++## Complete Example++Here's a more complete example showing how to create, validate, and send a CloudEvent to Kafka:++```haskell+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE OverloadedRecordDot #-}++module Example where++import Autodocodec+import CloudEvent.V1.Bindings.Kafka+import CloudEvent.V1.Event.Data+import CloudEvent.V1.Event.Lens+import CloudEvent.V1.Event.Validation+import Control.Lens+import Data.Aeson (FromJSON, ToJSON, toJSON)+import Data.Text (Text)+import GHC.Generics (Generic)+import Kafka.Producer++-- Define our event data type+data OrderCreatedEvent = OrderCreatedEvent+  { orderId :: Text+  , customerId :: Text+  , amount :: Double+  , items :: [Text]+  }+  deriving stock (Eq, Show, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec OrderCreatedEvent)++instance HasCodec OrderCreatedEvent where+  codec =+    object "OrderCreatedEvent" $+      OrderCreatedEvent+        <$> requiredField' "orderId" .= (.orderId)+        <*> requiredField' "customerId" .= (.customerId)+        <*> requiredField' "amount" .= (.amount)+        <*> requiredField' "items" .= (.items)++-- Define our Kafka-compatible extension+data OrderExtension = OrderExtension+  { partitionKey :: Text  -- For Kafka routing+  , region :: Text        -- Custom extension field+  }+  deriving stock (Eq, Show, Generic)++instance HasCodec OrderExtension where+  codec = object "OrderExtension" objectCodec++instance HasObjectCodec OrderExtension where+  objectCodec =+    OrderExtension+      <$> requiredField' "partitionKey" .= (.partitionKey)+      <*> requiredField' "region" .= (.region)++-- Create our CloudEvent+createOrderEvent :: Text -> OrderCreatedEvent -> OrderExtension -> CloudEvent OrderExtension OrderCreatedEvent+createOrderEvent orderId orderData ext =+  mkCloudEvent+    MkCloudEventArg+      { ceId = "order-" <> orderId+      , ceSource = -- ... (Iri value)+      , ceEventType = "com.example.order.created"+      , ceDataContentType = Just "application/json"+      , ceDataSchema = Nothing+      , ceSubject = Just ("order/" <> orderId)+      , ceTime = -- ... (UTCTime value)+      }+    orderData+    ext++-- Example usage in a function that processes and sends an order event+processOrder :: Text -> Text -> Double -> [Text] -> Text -> IO ()+processOrder orderId customerId amount items region = do+  -- Create the order data+  let orderData = OrderCreatedEvent+        { orderId = orderId+        , customerId = customerId+        , amount = amount+        , items = items+        }++      -- Create extension with Kafka partition key and region+      extension = OrderExtension+        { partitionKey = orderId  -- Use orderId as partition key+        , region = region+        }++      -- Create the CloudEvent+      event = createOrderEvent orderId orderData extension++      -- Define Kafka topic+      topic = "orders"++      -- Create Kafka producer record in binary mode+      producerRecord = toBinaryKafkaMessage topic event++  -- Validate the event before sending+  -- In real code, you would decode the JSON first+  let isValid = isValidCloudEvent @OrderCreatedEvent @OrderExtension (toJSON event)++  if isValid+    then do+      putStrLn "Event is valid, sending to Kafka..."+      -- In a real application, you would send to Kafka:+      -- producer <- newProducer producerProperties+      -- sendMessageBatch producer [producerRecord]+      -- closeProducer producer+      pure ()+    else+      putStrLn "Event validation failed!"+```++## Contributing++Contributions are welcome! Please feel free to submit a Pull Request.++## License++This project is licensed under the BSD-3-Clause License - see the [LICENSE](LICENSE) file for details.
+ cloudevents-haskell.cabal view
@@ -0,0 +1,75 @@+cabal-version:      2.2+name:               cloudevents-haskell+version:            0.1.0.0+license:            BSD-3-Clause+license-file:       LICENSE+maintainer:         Freckle Education+homepage:           https://github.com/freckle/cloudevents-haskell#readme+bug-reports:        https://github.com/freckle/cloudevents-haskell/issues+synopsis:           Unofficial Haskell SDK for the CloudEvents specification+description:+    See the README on GitHub at <https://github.com/freckle/cloudevents-haskell#readme>++category:           Utils+build-type:         Simple+extra-source-files: package.yaml+extra-doc-files:+    README.md+    CHANGELOG.md++source-repository head+    type:     git+    location: https://github.com/freckle/cloudevents-haskell++library+    exposed-modules:+        CloudEvent.V1.Bindings.Kafka+        CloudEvent.V1.Event.Data+        CloudEvent.V1.Event.Internal.Data+        CloudEvent.V1.Event.Internal.Orphans+        CloudEvent.V1.Event.Lens+        CloudEvent.V1.Event.Validation++    hs-source-dirs:     src+    other-modules:      Paths_cloudevents_haskell+    autogen-modules:    Paths_cloudevents_haskell+    default-language:   GHC2021+    default-extensions:+        ApplicativeDo DataKinds DeriveAnyClass DerivingStrategies+        DerivingVia DuplicateRecordFields GADTs LambdaCase NoFieldSelectors+        NoImplicitPrelude NoMonomorphismRestriction NoPostfixOperators+        OverloadedRecordDot OverloadedStrings QuasiQuotes TypeFamilies++    ghc-options:+        -Weverything -Wno-all-missed-specialisations+        -Wno-missed-specialisations -Wno-missing-exported-signatures+        -Wno-missing-import-lists -Wno-missing-local-signatures+        -Wno-monomorphism-restriction -Wno-safe -Wno-unsafe++    build-depends:+        aeson >=2.0.3.0,+        autodocodec >=0.4.2.2,+        autodocodec-schema >=0.2.0.1,+        base >=4.16.4.0 && <5,+        binary >=0.8.9.0,+        binary-instances >=1.0.4,+        bytestring >=0.11.4.0,+        hw-kafka-client >=5.3.0,+        iri >=0.5.1.1,+        lens >=5.1.1,+        text >=2.0.2,+        time >=1.12.2++    if impl(ghc >=9.8)+        ghc-options:+            -Wno-missing-role-annotations -Wno-missing-poly-kind-signatures++    if impl(ghc >=9.2)+        ghc-options: -Wno-missing-kind-signatures++    if impl(ghc >=8.10)+        ghc-options:+            -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module++    if impl(ghc >=8.8)+        ghc-options: -fwrite-ide-info
+ package.yaml view
@@ -0,0 +1,89 @@+name: cloudevents-haskell+version: 0.1.0.0+maintainer: Freckle Education+category: Utils+github: freckle/cloudevents-haskell+license: BSD-3-Clause+synopsis: Unofficial Haskell SDK for the CloudEvents specification+description: See the README on GitHub at <https://github.com/freckle/cloudevents-haskell#readme>++extra-doc-files:+  - README.md+  - CHANGELOG.md++extra-source-files:+  - package.yaml++language: GHC2021++dependencies:+  - base < 5++ghc-options:+  - -Weverything+  - -Wno-all-missed-specialisations+  - -Wno-missed-specialisations+  - -Wno-missing-exported-signatures # re-enables missing-signatures+  - -Wno-missing-import-lists+  - -Wno-missing-local-signatures+  - -Wno-monomorphism-restriction+  - -Wno-safe+  - -Wno-unsafe++when:+  - condition: "impl(ghc >= 9.8)"+    ghc-options:+      - -Wno-missing-role-annotations+      - -Wno-missing-poly-kind-signatures+  - condition: "impl(ghc >= 9.2)"+    ghc-options:+      - -Wno-missing-kind-signatures+  - condition: "impl(ghc >= 8.10)"+    ghc-options:+      - -Wno-missing-safe-haskell-mode+      - -Wno-prepositive-qualified-module+  - condition: "impl(ghc >= 8.8)"+    ghc-options:+      - -fwrite-ide-info++default-extensions:+  - ApplicativeDo+  - DataKinds+  - DeriveAnyClass+  - DerivingStrategies+  - DerivingVia+  - DuplicateRecordFields+  - GADTs+  - LambdaCase+  - NoFieldSelectors+  - NoImplicitPrelude+  - NoMonomorphismRestriction+  - NoPostfixOperators+  - OverloadedRecordDot+  - OverloadedStrings+  - QuasiQuotes+  - TypeFamilies++library:+  source-dirs: src+  dependencies:+    - aeson+    - autodocodec+    - autodocodec-schema+    - binary+    - binary-instances+    - bytestring+    - hw-kafka-client+    - iri+    - lens+    - text+    - time+# tests:+#   spec:+#     main: Spec.hs+#     source-dirs: test+#     ghc-options: -threaded -rtsopts "-with-rtsopts=-N"+#     dependencies:+#       - cloudevents-haskell+#       - hspec+#       - hspec-junit-formatter
+ src/CloudEvent/V1/Bindings/Kafka.hs view
@@ -0,0 +1,144 @@+-- | Provides functionality for integrating CloudEvents with Apache Kafka.+-- This module implements the CloudEvents Kafka Protocol Binding specification v1.0.2+-- which defines how CloudEvents are mapped to Kafka messages.+--+-- The binding supports two content modes:+--+-- * Binary mode: The CloudEvent's data is placed directly in the Kafka message value,+--   with all other attributes being placed in the message headers with a 'ce_' prefix.+--+-- * Structured mode: The entire CloudEvent, including its data, is serialized into a+--   single message value using the JSON event format.+--+-- See: <https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/bindings/kafka-protocol-binding.md>+module CloudEvent.V1.Bindings.Kafka+  ( KafkaCloudEventExtAttributes (..)+  , toBinaryKafkaMessage+  , toStructuredKafkaMessage+  ) where++import Prelude++import Autodocodec+  ( Autodocodec (..)+  , HasCodec (..)+  , HasObjectCodec (..)+  , object+  , requiredField'+  , (.=)+  )+import CloudEvent.V1.Event.Internal.Data (CloudEvent (..))+import Data.Aeson (FromJSON, ToJSON, encode)+import Data.Binary (Binary)+import Data.ByteString (ByteString)+import Data.ByteString.Lazy qualified as BSL+import Data.Maybe (catMaybes)+import Data.String (IsString (..))+import Data.Text (Text)+import Data.Text.Encoding (encodeUtf8)+import GHC.Generics (Generic)+import GHC.Records (HasField)+import Iri.Rendering.Text (iri)+import Kafka.Producer+  ( ProducePartition (UnassignedPartition)+  , ProducerRecord (..)+  , TopicName+  , headersFromList+  )++-- | Extension attributes specific to Kafka CloudEvents.+-- This follows the CloudEvents Kafka Protocol Binding specification which+-- recommends mapping a partition key attribute to the Kafka record key.+newtype KafkaCloudEventExtAttributes = KafkaCloudEventExtAttributes+  { partitionKey :: Text+  -- ^ The partition key used to determine which partition a message should be sent to.+  -- This corresponds to the 'partitionkey' attribute in the CloudEvents Partitioning extension.+  -- This will be used as the Kafka message key when sending events.+  }+  deriving stock (Eq, Generic, Show)+  deriving (FromJSON, ToJSON) via (Autodocodec KafkaCloudEventExtAttributes)+  deriving anyclass (Binary)++instance HasCodec KafkaCloudEventExtAttributes where+  codec = object "KafkaCloudEventExtAttributes" objectCodec++instance HasObjectCodec KafkaCloudEventExtAttributes where+  objectCodec =+    KafkaCloudEventExtAttributes+      <$> requiredField' "partitionKey" .= (.partitionKey)++-- | Converts a CloudEvent to a Kafka message in binary content mode.+--+-- In binary mode:+-- * The CloudEvent data is serialized as JSON and placed in the Kafka message value+-- * CloudEvent attributes are mapped to Kafka headers with 'ce_' prefix+-- * The content type header indicates the data format+-- * The partition key extension attribute is mapped to the Kafka message key+--+-- As specified in the Kafka Protocol Binding section 3.2:+-- <https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/bindings/kafka-protocol-binding.md#32-binary-content-mode>+toBinaryKafkaMessage+  :: forall ext eventData+   . (HasField "partitionKey" ext Text, ToJSON eventData)+  => TopicName+  -- ^ The Kafka topic to publish to+  -> CloudEvent ext eventData+  -- ^ The CloudEvent to convert+  -> ProducerRecord+  -- ^ Resulting Kafka producer record+toBinaryKafkaMessage topicName cloudEvent =+  let+    partitionKey = cloudEvent.extensionAttributes.partitionKey+    kafkaHeadersMapping :: [(ByteString, ByteString)] =+      catMaybes+        [ Just ("ce_id", encodeUtf8 cloudEvent.ceId)+        , Just ("ce_type", encodeUtf8 cloudEvent.ceType)+        , Just ("ce_source", encodeUtf8 $ iri $ cloudEvent.ceSource)+        , Just ("ce_specversion", encodeUtf8 cloudEvent.ceSpecVersion)+        , Just ("ce_time", fromString $ show $ cloudEvent.ceTime)+        , ("ce_subject",) . encodeUtf8 <$> cloudEvent.ceSubject+        , ("content-type",) . encodeUtf8 <$> cloudEvent.ceDataContentType+        , ("ce_dataschema",) . encodeUtf8 . iri <$> cloudEvent.ceDataSchema+        ]+  in+    ProducerRecord+      { prValue = Just $ BSL.toStrict $ encode cloudEvent.ceData+      , prTopic = topicName+      , prPartition = UnassignedPartition+      , prKey = Just $ encodeUtf8 partitionKey+      , prHeaders = headersFromList kafkaHeadersMapping+      }++-- | Converts a CloudEvent to a Kafka message in structured content mode AKA JSON.+--+-- In structured mode:+-- * The entire CloudEvent (attributes and data) is serialized as JSON and placed in the Kafka message value+-- * A 'content-type' header with value 'application/cloudevents+json; charset=utf-8' is added+-- * The partition key extension attribute is mapped to the Kafka message key+--+-- This format allows for simple forwarding of the same event across multiple+-- routing hops and protocols without requiring knowledge of the CloudEvent format.+--+-- As specified in the Kafka Protocol Binding section 3.3:+-- <https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/bindings/kafka-protocol-binding.md#33-structured-content-mode>+toStructuredKafkaMessage+  :: (HasCodec eventData, HasField "partitionKey" ext Text, HasObjectCodec ext)+  => TopicName+  -- ^ The Kafka topic to publish to+  -> CloudEvent ext eventData+  -- ^ The CloudEvent to convert+  -> ProducerRecord+  -- ^ Resulting Kafka producer record+toStructuredKafkaMessage topicName cloudEvent =+  let+    partitionKey = cloudEvent.extensionAttributes.partitionKey+    kafkaHeadersMapping :: [(ByteString, ByteString)] =+      [("content-type", "application/cloudevents+json; charset=utf-8")]+  in+    ProducerRecord+      { prValue = Just $ BSL.toStrict $ encode cloudEvent+      , prTopic = topicName+      , prPartition = UnassignedPartition+      , prKey = Just $ encodeUtf8 partitionKey+      , prHeaders = headersFromList kafkaHeadersMapping+      }
+ src/CloudEvent/V1/Event/Data.hs view
@@ -0,0 +1,83 @@+module CloudEvent.V1.Event.Data+  ( CloudEvent+  , MkCloudEventArg (..)+  , mkCloudEvent+  , EmptyExtension (..)+  ) where++import Prelude++import Autodocodec+  ( Autodocodec (..)+  , HasCodec (..)+  , HasObjectCodec (objectCodec)+  , object+  )+import CloudEvent.V1.Event.Internal.Data (CloudEvent (..))+import Data.Aeson (FromJSON, ToJSON)+import Data.Binary (Binary)+import Data.Text (Text)+import Data.Time (UTCTime)+import GHC.Generics (Generic)+import Iri.Data (Iri)++-- | Arguments for creating a CloudEvent+data MkCloudEventArg = MkCloudEventArg+  { ceId :: Text+  -- ^ Unique identifier for the event+  , ceSource :: Iri+  -- ^ Identifies the context where the event occurred+  , ceEventType :: Text+  -- ^ Describes the type of event+  , ceDataContentType :: Maybe Text+  -- ^ Content type of the event data (e.g., application/json)+  , ceDataSchema :: Maybe Iri+  -- ^ Schema that the event data adheres to+  , ceSubject :: Maybe Text+  -- ^ Subject of the event within the context+  , ceTime :: UTCTime+  -- ^ Timestamp when the event occurred+  }++mkCloudEvent+  :: MkCloudEventArg+  -> eventData+  -> extensionAttributeRecord+  -> CloudEvent extensionAttributeRecord eventData+mkCloudEvent x eventData ext =+  CloudEvent+    { ceId = x.ceId+    , ceSource = x.ceSource+    , ceSpecVersion = "1.0"+    , ceType = x.ceEventType+    , ceDataContentType = x.ceDataContentType+    , ceDataSchema = x.ceDataSchema+    , ceSubject = x.ceSubject+    , ceTime = x.ceTime+    , ceData = eventData+    , ceDatabase64 = Nothing+    , extensionAttributes = ext+    }++-- | A placeholder type representing an empty set of extension attributes.+--+-- This type is useful when you need to create a CloudEvent with no extension+-- attributes but require a concrete type parameter for the extension.+-- It encodes and decodes to an empty JSON object.+--+-- Example usage:+--+-- @+-- myEvent :: CloudEvent EmptyExtension MyDataType+-- myEvent = mkCloudEvent args myData EmptyExtension+-- @+data EmptyExtension = EmptyExtension+  deriving stock (Eq, Generic, Show)+  deriving anyclass (Binary)+  deriving (FromJSON, ToJSON) via (Autodocodec EmptyExtension)++instance HasCodec EmptyExtension where+  codec = object "EmptyExtension" objectCodec++instance HasObjectCodec EmptyExtension where+  objectCodec = pure EmptyExtension
+ src/CloudEvent/V1/Event/Internal/Data.hs view
@@ -0,0 +1,124 @@+module CloudEvent.V1.Event.Internal.Data+  ( CloudEvent (..)+  ) where++import Prelude++import Autodocodec+  ( Autodocodec (..)+  , HasCodec (..)+  , HasObjectCodec (objectCodec)+  , object+  , optionalField'+  , requiredField'+  , (.=)+  )+import CloudEvent.V1.Event.Internal.Orphans ()+import Data.Aeson (FromJSON, ToJSON)+import Data.Binary (Binary)+import Data.Text (Text)+import Data.Time (UTCTime)+import GHC.Generics (Generic)+import Iri.Data (Iri)++-- | A CloudEvent represents the standardized format for describing event data.+-- This implementation follows the CloudEvents specification version 1.0.+-- CloudEvents are designed to provide interoperability across services,+-- platforms and systems.+data CloudEvent extension eventData = CloudEvent+  { ceId :: Text+  -- ^ Identifies the event. Producers must ensure that 'source' + 'id' is unique+  -- for each distinct event. If a duplicate event is re-sent (e.g. due to a network error)+  -- it may have the same 'id'. Consumers may assume that events with identical+  -- 'source' and 'id' are duplicates.+  --+  -- Examples:+  -- * An event counter maintained by the producer+  -- * A UUID+  , ceSource :: Iri+  -- ^ Identifies the context in which an event happened. Often this will include+  -- information such as the type of the event source, the organization publishing the event,+  -- or the process that produced the event.+  --+  -- Examples:+  -- * https://github.com/cloudevents+  -- * mailto:cncf-wg-serverless@lists.cncf.io+  -- * urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66+  -- * /sensors/tn-1234567/alerts+  , ceSpecVersion :: Text+  -- ^ The version of the CloudEvents specification which the event uses.+  -- This enables the interpretation of the context.+  -- For CloudEvents Spec v1.0, this is always "1.0".+  , ceType :: Text+  -- ^ This attribute contains a value describing the type of event related to the+  -- originating occurrence. Often this attribute is used for routing, observability,+  -- policy enforcement, etc.+  --+  -- Examples:+  -- * com.github.pull_request.opened+  -- * com.example.object.deleted.v2+  , ceDataContentType :: Maybe Text+  -- ^ Content type of the data value. This attribute enables data to carry any+  -- type of content, whereby format and encoding might differ from that of the chosen+  -- event format. For example, an event rendered using the JSON format might carry an+  -- XML payload in data.+  --+  -- Examples:+  -- * text/xml+  -- * application/json+  -- * image/png+  -- * application/octet-stream+  , ceDataSchema :: Maybe Iri+  -- ^ Identifies the schema that data adheres to. Incompatible changes to the schema+  -- should be reflected by a different URI.+  , ceSubject :: Maybe Text+  -- ^ This describes the subject of the event in the context of the event producer.+  -- In publish-subscribe scenarios, a subscriber will typically subscribe to events+  -- emitted by a source, but the source identifier alone might not be sufficient as+  -- a qualifier for any specific event if the source context has internal sub-structure.+  --+  -- Example:+  -- * A storage system might have an event source (storage container) but include the name+  --   of the modified file in the subject: "mynewfile.jpg"+  , ceTime :: UTCTime+  -- ^ Timestamp of when the occurrence happened. If the time of the occurrence+  -- cannot be determined then this attribute may be set to some other time+  -- (such as the current time) by the CloudEvents producer.+  --+  -- Example:+  -- * 2018-04-05T17:31:00Z+  , ceData :: eventData+  -- ^ The actual event data. This may be structured data formatted according to+  -- the 'datacontenttype' attribute. This specification does not place any restriction+  -- on the type of this information.+  , ceDatabase64 :: Maybe Text+  -- ^ Base64 encoded event payload. Must adhere to RFC4648.+  -- This is used for data that is binary in nature and can't be directly included+  -- in standard formats like JSON.+  , extensionAttributes :: extension+  -- ^ User-defined extension attributes to the CloudEvent. Extension attributes+  -- have no defined meaning in the core CloudEvent specification.+  -- They allow external systems to attach metadata to an event.+  }+  deriving stock (Eq, Generic, Show)+  deriving (FromJSON, ToJSON) via (Autodocodec (CloudEvent extension eventData))+  deriving anyclass (Binary)++instance+  (HasCodec eventData, HasObjectCodec extension)+  => HasCodec (CloudEvent extension eventData)+  where+  codec =+    object "CloudEvent"+      $ CloudEvent+        <$> requiredField' "id" .= (.ceId)+        <*> requiredField' "source" .= (.ceSource)+        <*> requiredField' "specversion" .= (.ceSpecVersion)+        <*> requiredField' "type" .= (.ceType)+        <*> optionalField' "datacontenttype" .= (.ceDataContentType)+        <*> optionalField' "dataschema" .= (.ceDataSchema)+        <*> optionalField' "subject" .= (.ceSubject)+        <*> requiredField' "time" .= (.ceTime)+        <*> requiredField' "data" .= (.ceData)+        <*> optionalField' "data_base64" .= (.ceDatabase64)+        <*> objectCodec @extension .= (.extensionAttributes)
+ src/CloudEvent/V1/Event/Internal/Orphans.hs view
@@ -0,0 +1,32 @@+{-# OPTIONS_GHC -Wno-missing-export-lists #-}+{-# OPTIONS_GHC -Wno-orphans #-}++module CloudEvent.V1.Event.Internal.Orphans where++import Prelude++import Autodocodec (Autodocodec (..), HasCodec (codec), bimapCodec)+import Data.Aeson (FromJSON, ToJSON)+import Data.Bifunctor (first)+import Data.Binary (Binary (..))+import Data.Binary.Instances ()+import Data.Text (Text)+import Data.Text qualified as T+import Iri.Data (Iri)+import Iri.Parsing.Text qualified+import Iri.Rendering.Text qualified++instance HasCodec Iri where+  codec =+    bimapCodec+      (first T.unpack . Iri.Parsing.Text.iri)+      Iri.Rendering.Text.iri+      (codec @Text)++deriving via Autodocodec Iri instance FromJSON Iri++deriving via Autodocodec Iri instance ToJSON Iri++instance Binary Iri where+  put r = put @Text $ Iri.Rendering.Text.iri r+  get = get @Text >>= either (fail . T.unpack) pure . Iri.Parsing.Text.iri
+ src/CloudEvent/V1/Event/Lens.hs view
@@ -0,0 +1,22 @@+{-# LANGUAGE TemplateHaskell #-}+{-# OPTIONS_GHC -Wno-missing-export-lists #-}++module CloudEvent.V1.Event.Lens where++import CloudEvent.V1.Event.Data (CloudEvent)+import Control.Lens (makeLensesFor)++makeLensesFor+  [ ("ceId", "_ceId")+  , ("ceSource", "_ceSource")+  , ("ceSpecversion", "_ceSpecversion")+  , ("ceType", "_ceType")+  , ("ceDatacontenttype", "_ceDatacontenttype")+  , ("ceDataschema", "_ceDataschema")+  , ("ceSubject", "_ceSubject")+  , ("ceTime", "_ceTime")+  , ("ceData", "_ceData")+  , ("ceData_base64", "_ceData_base64")+  , ("extensionAttributes", "_extensionAttributes")+  ]+  ''CloudEvent
+ src/CloudEvent/V1/Event/Validation.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE AllowAmbiguousTypes #-}++module CloudEvent.V1.Event.Validation+  ( isValidCloudEvent+  ) where++import Prelude++import Autodocodec (HasCodec, HasObjectCodec)+import Autodocodec.Schema (jsonSchemaViaCodec, validateAccordingTo)+import CloudEvent.V1.Event.Data (CloudEvent)+import Data.Aeson (Value)+import Data.Kind (Type)++-- | Validates that a JSON value conforms to a specific CloudEvents specification.+--  Requires the event data type and extension attributes to be specified as type applications with the event data type first.+--+-- Example usage:+--+-- @+-- let jsonValue = ...  -- Some JSON representation of a CloudEvent+-- if isValidCloudEvent \@MyEventData \@MyExtensions jsonValue+--   then handleValidEvent jsonValue+--   else handleInvalidEvent+-- @+isValidCloudEvent+  :: forall (eventData :: Type) (extensionAttributes :: Type)+   . (HasCodec eventData, HasObjectCodec extensionAttributes) => Value -> Bool+isValidCloudEvent =+  flip+    validateAccordingTo+    ( jsonSchemaViaCodec+        @(CloudEvent extensionAttributes eventData)+    )