Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/ci_asyncapi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Validates the generated AsyncAPI spec via @asyncapi/cli on push and PR to main.
name: AsyncAPI

on:
push:
branches:
- main
pull_request:
branches:
- main

permissions:
contents: read

jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: '22'

- name: Set up Task
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: '3.40.1'

- name: Validate AsyncAPI spec
Comment thread
hbraswelrh marked this conversation as resolved.
run: task asyncapi-lint

2 changes: 1 addition & 1 deletion .github/workflows/ci_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ jobs:
run: go vet ./...

- name: Run tests
run: go test ./... -v
run: go test -race -count=1 ./... -v
74 changes: 71 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ Go library providing event contract types for the
[ComplyTime](https://github.com/complytime) ecosystem.

Events use [CloudEvents](https://cloudevents.io/) v1.0 envelopes with
JSON-encoded payloads. The canonical event contract is defined in the
[AsyncAPI 3.0 spec](api/events/asyncapi.yaml); the Go types in this
library must match that spec.
JSON-encoded payloads. Go types in `events/events.go` are the source of
truth for event contracts; the [AsyncAPI 3.0 spec](api/events/asyncapi.yaml)
and [JSON Schema files](api/events/schemas/) are generated from those types
via `go generate`. Do not edit the generated files manually.

## Installation

Expand Down Expand Up @@ -47,6 +48,73 @@ func main() {
|------|----------|-------------|
| `dev.complytime.evidence.ingested` | `events.TypeEvidenceIngested` | Evidence accepted for processing |

## Development

### Prerequisites

- [Go](https://go.dev/) (version per `go.mod`)
- [Task](https://taskfile.dev/) (task runner)
- [golangci-lint](https://golangci-lint.run/)
- [Node.js](https://nodejs.org/) with `npx` (required by `task asyncapi-lint`; first run downloads the AsyncAPI CLI from npm)

After modifying Go structs in `events/events.go`, regenerate derived artifacts:

```bash
task generate
```

This runs `go generate ./events/...` which rebuilds `api/events/asyncapi.yaml`
and the JSON Schema files in `api/events/schemas/`.

To validate the generated AsyncAPI spec:

```bash
task asyncapi-lint
```

To run all checks (lint, vet, test, asyncapi validation):

```bash
task check
```

### Adding a new event type

1. Define a new `*Data` struct in `events/events.go` with a sentinel blank
field carrying the `asyncapi` tag. The tag is a comma-separated list of
`key:value` pairs. **Values must not contain commas** (the parser splits
on commas, and a comma inside a value silently truncates it). Use
semicolons for natural pauses. Recognised keys:

| Key | Required | Format | Description |
|-----|----------|--------|-------------|
| `channel` | yes | NATS subject with `{param}` placeholders | Channel address |
| `param` | no | `name=description` (repeatable) | Channel parameter |
| `stream` | yes | Upper-case stream name | NATS JetStream stream |
| `type` | yes | Reverse-DNS CloudEvents type | CloudEvents `type` attribute |
| `send` | yes | Free text (no commas) | Send operation summary |
| `receive` | yes | Free text (no commas) | Receive operation summary |
| `description` | no | Free text (no commas) | Channel description |

Example sentinel field:
```go
_ struct{} `asyncapi:"channel:core.widget.created.{ownerId},param:ownerId=The widget owner,stream:WIDGETS,type:dev.complytime.widget.created,send:Published when a widget is created,receive:Consume widget-created events,description:Widget creation pipeline"`
```

2. Add `asyncapi-field:"description:..."` tags on each struct field for
schema descriptions.
3. Run `task generate` to regenerate all derived artifacts.
4. Add a constructor function (e.g., `NewYourEventEvent()`) following the
existing pattern.

### Evolving an event contract

Before changing an existing event's payload, read the
[Event Versioning Strategy](docs/versioning.md). It defines which field to bump
for additive versus breaking changes (CloudEvents `type` and AsyncAPI
`info.version`), and why the NATS subject stays stable so subscribers never
re-subscribe.

## License

[Apache-2.0](LICENSE)
16 changes: 14 additions & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ tasks:
test:
desc: Run all tests
cmds:
- go test ./...
- go test -race -count=1 ./...

lint:
desc: Run golangci-lint
Expand All @@ -17,9 +17,21 @@ tasks:
cmds:
- go vet ./...

generate:
desc: Regenerate derived artifacts (asyncapi.yaml and JSON Schemas)
cmds:
- go generate ./events/...

asyncapi-lint:
desc: Validate asyncapi.yaml with the AsyncAPI CLI
cmds:
# Pinned to 2.16.1 — latest has broken npm dependency (@asyncapi/studio-ui@0.5.0 404)
- npx --yes @asyncapi/cli@2.16.1 validate api/events/asyncapi.yaml

check:
desc: Run lint and tests
desc: Run lint, vet, tests, and asyncapi validation
cmds:
- task: lint
- task: vet
- task: test
- task: asyncapi-lint
222 changes: 108 additions & 114 deletions api/events/asyncapi.yaml
Original file line number Diff line number Diff line change
@@ -1,122 +1,116 @@
# SPDX-License-Identifier: Apache-2.0
asyncapi: 3.0.0
info:
title: ComplyTime API Events
version: 0.1.0
description: |
Event contract for the ComplyTime evidence lifecycle.

All public events use CloudEvents v1.0 envelope (JSON format).
The AsyncAPI spec is the source of truth for event contracts;
Go types in the events package must match these schemas.
license:
name: Apache-2.0
contact:
name: ComplyTime
url: https://github.com/complytime/complyapi
title: ComplyTime API Events
version: 0.1.0
description: |-
Event contract for the ComplyTime evidence lifecycle.

All public events use CloudEvents v1.0 envelope (JSON format).
This spec is generated from Go types in the events package via cmd/asyncapi-gen.
Do not edit manually — run 'go generate ./events/...' to regenerate.
license:
name: Apache-2.0
contact:
name: ComplyTime
url: https://github.com/complytime/complyapi
defaultContentType: application/cloudevents+json

servers:
nats:
host: localhost:4222
protocol: nats
channels:
evidenceIngested:
address: core.evidence.ingested.{subjectId}
description: |
Published when evidence is ingested, before sealing.
parameters:
subjectId:
description: The compliance subject identifier (e.g. `my-app-v1`)
messages:
evidenceIngested:
$ref: '#/components/messages/evidenceIngested'

evidenceIngested:
address: core.evidence.ingested.{subjectId}
description: Evidence ingestion pipeline for compliance artifacts
parameters:
subjectId:
description: The compliance subject identifier
messages:
EvidenceIngested:
$ref: '#/components/messages/EvidenceIngested'
operations:
publishEvidenceIngested:
action: send
channel:
$ref: '#/channels/evidenceIngested'
summary: Published when evidence is accepted for processing.

consumeEvidenceIngested:
action: receive
channel:
$ref: '#/channels/evidenceIngested'
summary: Consume evidence-ingested events.

consumeEvidenceIngested:
action: receive
summary: Consume evidence-ingested events
channel:
$ref: '#/channels/evidenceIngested'
publishEvidenceIngested:
action: send
summary: Published when evidence is accepted for processing; before sealing
channel:
$ref: '#/channels/evidenceIngested'
bindings:
nats:
x-stream: EVIDENCE
bindingVersion: 0.1.0
components:
messages:
evidenceIngested:
name: EvidenceIngested
title: Evidence Ingested
contentType: application/cloudevents+json
payload:
$ref: '#/components/schemas/EvidenceIngestedCloudEvent'

schemas:
EvidenceIngestedCloudEvent:
type: object
description: CloudEvents v1.0 envelope for evidence.ingested
required:
- specversion
- id
- type
- source
- subject
- time
- datacontenttype
- data
properties:
specversion:
type: string
const: "1.0"
id:
type: string
format: uuid
type:
type: string
const: dev.complytime.evidence.ingested
source:
type: string
description: URI identifying the producing service
examples:
- complytime-gateway
subject:
type: string
description: The compliance subject identifier
time:
type: string
format: date-time
datacontenttype:
type: string
const: application/json
data:
$ref: '#/components/schemas/EvidenceIngestedData'

EvidenceIngestedData:
type: object
description: Payload for evidence.ingested events.
required:
- contentDigest
- artifactType
- subjectId
properties:
contentDigest:
type: string
description: SHA-256 digest of the evidence artifact
examples:
- sha256:abc123...
artifactType:
type: string
description: Gemara artifact type
examples:
- application/vnd.gemara.evaluation-log+json
storageRef:
type: string
description: Internal storage reference
subjectId:
type: string
description: Compliance subject identifier
examples:
- my-app-v1
shardId:
type: string
description: Subject shard identifier (null when sharding is not configured)
messages:
EvidenceIngested:
name: EvidenceIngested
title: Evidence Ingested
contentType: application/cloudevents+json
payload:
$ref: '#/components/schemas/EvidenceIngestedCloudEvent'
schemas:
EvidenceIngestedCloudEvent:
type: object
description: CloudEvents v1.0 envelope for dev.complytime.evidence.ingested
required:
- specversion
- id
- type
- source
- subject
- time
- datacontenttype
- data
properties:
data:
$ref: '#/components/schemas/EvidenceIngestedData'
datacontenttype:
type: string
const: application/json
id:
type: string
format: uuid
source:
type: string
description: URI identifying the producing service
specversion:
type: string
const: "1.0"
subject:
type: string
description: The compliance subject identifier
time:
type: string
format: date-time
type:
type: string
const: dev.complytime.evidence.ingested
EvidenceIngestedData:
type: object
description: |-
EvidenceIngestedData is the CloudEvents data payload for
evidence.ingested events.
required:
- contentDigest
- artifactType
- subjectId
properties:
artifactType:
type: string
description: Gemara artifact type
contentDigest:
type: string
description: SHA-256 digest of the evidence artifact
shardId:
type: string
description: Subject shard identifier (null when sharding is not configured)
storageRef:
type: string
description: Internal storage reference
subjectId:
type: string
description: Compliance subject identifier
Loading
Loading