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 +5/−0
- LICENSE +21/−0
- README.md +363/−0
- cloudevents-haskell.cabal +75/−0
- package.yaml +89/−0
- src/CloudEvent/V1/Bindings/Kafka.hs +144/−0
- src/CloudEvent/V1/Event/Data.hs +83/−0
- src/CloudEvent/V1/Event/Internal/Data.hs +124/−0
- src/CloudEvent/V1/Event/Internal/Orphans.hs +32/−0
- src/CloudEvent/V1/Event/Lens.hs +22/−0
- src/CloudEvent/V1/Event/Validation.hs +34/−0
+ 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)+ )