-
Notifications
You must be signed in to change notification settings - Fork 2
feat: automated AsyncAPI generation from Go structs #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cdff097
d642d28
4b01511
18a7636
b6377e8
cd0826a
3acea3a
f838fcf
7924cf9
fb87303
46e9e40
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,122 +1,107 @@ | ||
| # 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). | ||
| 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 | ||
| 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} | ||
| 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 | ||
| 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: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consumers would use the public contract or build and then extract the schemas as JSON from |
||
| 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: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Go struct output |
||
| type: object | ||
| required: | ||
| - contentDigest | ||
| - artifactType | ||
| - subjectId | ||
| properties: | ||
| artifactType: | ||
| type: string | ||
| contentDigest: | ||
| type: string | ||
| shardId: | ||
| type: string | ||
| storageRef: | ||
| type: string | ||
| subjectId: | ||
| type: string | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
| "testing" | ||
| ) | ||
|
|
||
| // TestIntegration_GeneratedMatchesCommitted regenerates asyncapi.yaml from | ||
| // events/events.go and verifies the output matches the committed file. | ||
| // This is the drift detector: it fails if the two are out of sync. | ||
| func TestIntegration_GeneratedMatchesCommitted(t *testing.T) { | ||
| // Path to the real events source, relative to this test file location. | ||
| inputPath := filepath.Join("..", "..", "events", "events.go") | ||
| committedPath := filepath.Join("..", "..", "api", "events", "asyncapi.yaml") | ||
|
|
||
| specs, err := ParseFile(inputPath) | ||
| if err != nil { | ||
| t.Fatalf("ParseFile: %v", err) | ||
| } | ||
|
|
||
| doc := BuildDoc(specs, "ComplyTime API Events", "0.1.0", | ||
| "Event contract for the ComplyTime evidence lifecycle.\n\nAll public events use CloudEvents v1.0 envelope (JSON format).\nThe AsyncAPI spec is the source of truth for event contracts;\nGo types in the events package must match these schemas.", | ||
| "Apache-2.0", "ComplyTime", "https://github.com/complytime/complyapi", "nats://localhost:4222") | ||
|
|
||
| outPath := filepath.Join(t.TempDir(), "asyncapi.yaml") | ||
| if err := WriteYAML(doc, outPath); err != nil { | ||
| t.Fatalf("WriteYAML: %v", err) | ||
| } | ||
|
|
||
| generated, err := os.ReadFile(outPath) | ||
| if err != nil { | ||
| t.Fatalf("reading generated file: %v", err) | ||
| } | ||
| committed, err := os.ReadFile(committedPath) | ||
| if err != nil { | ||
| t.Fatalf("reading committed file: %v", err) | ||
| } | ||
|
|
||
| if string(generated) != string(committed) { | ||
| t.Errorf("generated asyncapi.yaml does not match committed file.\n"+ | ||
| "Run `go generate ./events/...` to update it.\n\n"+ | ||
| "--- committed\n+++ generated\n%s", | ||
| diffStrings(string(committed), string(generated)), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| // diffStrings returns a simple line-diff between a and b. | ||
| func diffStrings(a, b string) string { | ||
| aLines := splitLines(a) | ||
| bLines := splitLines(b) | ||
| var out []string | ||
| max := len(aLines) | ||
| if len(bLines) > max { | ||
| max = len(bLines) | ||
| } | ||
| for i := 0; i < max; i++ { | ||
| var al, bl string | ||
| if i < len(aLines) { | ||
| al = aLines[i] | ||
| } | ||
| if i < len(bLines) { | ||
| bl = bLines[i] | ||
| } | ||
| if al != bl { | ||
| out = append(out, fmt.Sprintf("line %d:\n committed: %q\n generated: %q", i+1, al, bl)) | ||
| } | ||
| } | ||
| if len(out) == 0 { | ||
| return "(no line differences found — may be whitespace)" | ||
| } | ||
| return strings.Join(out, "\n") | ||
| } | ||
|
|
||
| func splitLines(s string) []string { | ||
| return strings.Split(s, "\n") | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| // Command asyncapi-gen generates an AsyncAPI 3.0 document from annotated | ||
| // Go event structs. Run via go generate in the events package. | ||
| package main | ||
|
|
||
| import ( | ||
| "flag" | ||
| "fmt" | ||
| "os" | ||
| ) | ||
|
|
||
| func main() { | ||
| input := flag.String("input", "", "Path to Go source file containing annotated event structs (required)") | ||
| output := flag.String("output", "", "Path to write the generated asyncapi.yaml (required)") | ||
| title := flag.String("title", "", "AsyncAPI document title (required)") | ||
| version := flag.String("version", "", "AsyncAPI document version (required)") | ||
| server := flag.String("server", "", "NATS server URL, e.g. nats://localhost:4222 (required)") | ||
| description := flag.String("description", "", "AsyncAPI document description (optional)") | ||
| licenseName := flag.String("license", "", "License name, e.g. Apache-2.0 (optional)") | ||
| contactName := flag.String("contact-name", "", "Contact name (optional)") | ||
| contactURL := flag.String("contact-url", "", "Contact URL (optional)") | ||
| flag.Parse() | ||
|
|
||
| if *input == "" || *output == "" || *title == "" || *version == "" || *server == "" { | ||
| fmt.Fprintln(os.Stderr, "asyncapi-gen: all flags are required: -input -output -title -version -server") | ||
| flag.Usage() | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| specs, err := ParseFile(*input) | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "asyncapi-gen: parse error: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
| if len(specs) == 0 { | ||
| fmt.Fprintln(os.Stderr, "asyncapi-gen: no annotated structs found in input file") | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| doc := BuildDoc(specs, *title, *version, *description, *licenseName, *contactName, *contactURL, *server) | ||
|
|
||
| if err := WriteYAML(doc, *output); err != nil { | ||
| fmt.Fprintf(os.Stderr, "asyncapi-gen: write error: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| fmt.Printf("asyncapi-gen: wrote %s (%d event(s))\n", *output, len(specs)) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jpower432 great! This is actually what I was referencing in my comment on https://github.com/complytime/nunya/issues/430 here