Skip to content

feat: OpenAPI spec composition and local mock server with event emission#6

Merged
disk0Dancer merged 10 commits into
mainfrom
copilot/add-microservices-api-facade
Apr 22, 2026
Merged

feat: OpenAPI spec composition and local mock server with event emission#6
disk0Dancer merged 10 commits into
mainfrom
copilot/add-microservices-api-facade

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 21, 2026

Adds two new top-level capabilities: composing multiple microservice OpenAPI specs into a single facade CLI, and spinning up a local HTTP mock server from any spec.

climate compose — multi-spec facade

Merges N OpenAPI specs (each assigned a path prefix) into one composite spec, then generates a CLI from it. Useful for teams with multiple microservices who want a single gateway CLI.

climate compose orders.yaml:/api/orders users.yaml:/api/users
climate compose --name gateway --title "My Gateway API" \
  https://orders.svc/openapi.json:/orders \
  https://users.svc/openapi.json:/users

Merge logic (internal/compose):

  • Prepends the caller-specified prefix to every path key
  • Namespaces components/schemas and components/parameters with a slug derived from the prefix (e.g. /api/ordersapi-orders-) to prevent cross-service collisions
  • Rewrites all $ref strings in parameters, request bodies, and responses to point at the new namespaced names
  • De-duplicates tags; security schemes are merged with last-writer-wins behavior

climate mock — local simulator

Starts an HTTP server that serves synthetic JSON for every endpoint in a spec. No credentials, no side-effects, works spec-first before the real service exists.

climate mock ./openapi.yaml
climate mock --port 9090 --latency 200 https://petstore3.swagger.io/api/v3/openapi.json

Response generation (internal/mock):

  • Selects the first 2xx response schema per operation
  • Recursively generates values: object → all declared properties, array → one-element array, scalars → zero values, $ref → resolved and generated (max depth 4)
  • Path parameters ({petId}) compiled to regexps; longer paths matched first
  • Returns 405 with Allow header for undefined methods, 404 for unregistered paths
  • --latency <ms> adds uniform artificial delay

Event/webhook simulation mode

climate mock now also supports one-shot synthetic event emission for webhook-style integrations:

climate mock \
  --emit-url http://localhost:3001/webhooks/order-created \
  --event-path /events/order-created \
  --event-method POST \
  ./openapi.yaml

Event emission behavior:

  • --emit-url enables emit mode (send one event and exit)
  • --event-path selects the OpenAPI operation used to generate payload
  • --event-method chooses HTTP method (validated; defaults to POST)
  • Payload generation prefers operation requestBody schema (application/json first), then falls back to first successful 2xx response schema
  • Outbound request is context-aware with timeout and JSON Content-Type

Additional follow-up updates from feedback

  • Updated skills documentation to reflect new commands and workflows:
    • skills/climate.md
    • skills/climate-generator/SKILL.md
  • Added OpenAPI capability roadmap and design coverage doc:
    • docs/openapi-3-support-matrix.md (implemented vs planned support, including webhooks/event APIs and pagination strategy)
  • Added contributor workflow guide:
    • AGENTS.md (design → docs → tests → code → skills → validation/CI workflow)
  • Updated top-level docs indexing for new command/docs links:
    • README.md
    • docs/index.md

Design docs

  • docs/design-compose.md — algorithm walkthrough, auth strategy, namespacing table, limitations
  • docs/design-mock.md — request matching, response generation table, limitations, and event/webhook simulation mode
  • docs/openapi-3-support-matrix.md — OpenAPI 3.x support matrix and roadmap

Copilot AI and others added 2 commits April 20, 2026 21:19
@disk0Dancer disk0Dancer marked this pull request as ready for review April 21, 2026 12:30
Copilot AI review requested due to automatic review settings April 21, 2026 12:30
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds two new CLI capabilities to the climate toolchain: composing multiple OpenAPI specs into a single “gateway” spec/CLI, and running a local HTTP mock server driven by an OpenAPI spec.

Changes:

  • Introduces internal/compose to load/merge multiple OpenAPI specs (path prefixing + component namespacing + $ref rewriting) and exposes climate compose.
  • Introduces internal/mock to serve generated JSON responses for OpenAPI paths and exposes climate mock.
  • Adds design docs and initial unit tests for both features.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
internal/mock/mock.go Implements mock server routing, response selection, and schema-driven JSON generation.
internal/mock/mock_test.go Exercises mock server behavior for routing, status codes, content-type, latency, and $ref resolution.
internal/compose/compose.go Implements spec merge logic (prefixing, namespacing, $ref rewriting) and merge-to-bytes helper.
internal/compose/compose_test.go Verifies merge behavior for prefixing, defaults, namespacing, tag dedup, and basic error cases.
cmd/climate/commands/mock.go Adds climate mock command wiring (flags, load spec, print summary, serve).
cmd/climate/commands/compose.go Adds climate compose command wiring and spec:prefix argument parsing.
docs/design-mock.md Documents intended behavior/limitations of the mock server.
docs/design-compose.md Documents intended behavior/limitations of spec composition.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/mock/mock.go
Comment on lines +281 to +289
// parseStatusCode converts an OpenAPI response code string (e.g. "200",
// "default") to an integer. "default" maps to 200.
func parseStatusCode(code string) int {
if code == "default" {
return http.StatusOK
}
n, err := strconv.Atoi(code)
if err != nil {
return http.StatusOK
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseStatusCode treats any non-integer response code (e.g. OpenAPI pattern keys like "4XX") as HTTP 200. That means a "4XX" response could be mistakenly selected as the first successful 2xx response. Consider handling pattern keys explicitly (e.g. accept only "2XX" as a success) and returning a sentinel/error for non-numeric codes so they can be skipped.

Suggested change
// parseStatusCode converts an OpenAPI response code string (e.g. "200",
// "default") to an integer. "default" maps to 200.
func parseStatusCode(code string) int {
if code == "default" {
return http.StatusOK
}
n, err := strconv.Atoi(code)
if err != nil {
return http.StatusOK
// parseStatusCode converts an OpenAPI response code string (for example
// "200", "default", or "2XX") to an integer status code.
//
// "default" and the success pattern "2XX" map to 200. Other patterned or
// non-numeric values return 0 so callers can skip them instead of treating
// them as a successful 2xx response.
func parseStatusCode(code string) int {
normalized := strings.ToUpper(strings.TrimSpace(code))
switch normalized {
case "DEFAULT", "2XX":
return http.StatusOK
case "1XX", "3XX", "4XX", "5XX":
return 0
}
n, err := strconv.Atoi(normalized)
if err != nil {
return 0

Copilot uses AI. Check for mistakes.
Comment thread internal/mock/mock.go
sort.Strings(paths)
for _, path := range paths {
item := s.openAPI.Paths[path]
for method := range item.Operations() {
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary iterates over item.Operations() (a map), so method order within each path is nondeterministic. This can make CLI output and snapshots flaky. Consider collecting methods into a slice and sorting them before writing each "METHOD /path" line.

Suggested change
for method := range item.Operations() {
ops := item.Operations()
methods := make([]string, 0, len(ops))
for method := range ops {
methods = append(methods, method)
}
sort.Strings(methods)
for _, method := range methods {

Copilot uses AI. Check for mistakes.
Comment on lines +195 to +198
// Merge security schemes (last writer wins for same name).
for name, scheme := range src.Components.SecuritySchemes {
dst.Components.SecuritySchemes[name] = scheme
}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security schemes are currently merged without any namespacing (last-writer-wins on the original name). This contradicts the PR description / design doc that says security schemes are also namespaced, and it also risks collisions when multiple services use the same scheme key (e.g. "bearerAuth") but with different definitions. Consider namespacing security scheme keys the same way as schemas/parameters and rewriting any SecurityRequirement entries (root and per-operation) to the new scheme names, or update the docs/PR description to match the actual behavior.

Copilot uses AI. Check for mistakes.
Comment thread internal/mock/mock.go
Comment on lines +164 to +167
statusCode, body := s.generateResponse(op)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
if encErr := json.NewEncoder(w).Encode(body); encErr != nil {
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handler always sets Content-Type: application/json and JSON-encodes a response body even for status codes that must not include a body (e.g. 204 No Content). This can lead to protocol-inaccurate responses (and some clients will misbehave). Consider skipping Content-Type and not writing a body for 204/304 (and possibly for responses with no content schema).

Copilot uses AI. Check for mistakes.
Comment thread internal/mock/mock.go
Comment on lines +167 to +170
if encErr := json.NewEncoder(w).Encode(body); encErr != nil {
// The header is already written; log to stderr as best effort.
fmt.Fprintf(w, `{"error":"response encoding failed"}`)
}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If JSON encoding fails, this code writes an inline JSON string to the same ResponseWriter after headers have already been sent. This can result in a malformed/partial response (and the comment says "log to stderr" but it doesn't). Consider logging via a logger/stderr (or http.Error before WriteHeader), and returning without attempting to write additional body bytes after an encoding failure.

Copilot uses AI. Check for mistakes.
Comment thread internal/mock/mock.go
Comment on lines +202 to +208
// responseSchema extracts the first JSON schema from a response's content map.
func responseSchema(resp spec.Response) *spec.Schema {
for _, mt := range resp.Content {
if mt.Schema != nil {
return mt.Schema
}
}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

responseSchema iterates over resp.Content (a Go map) and returns the first schema it encounters. Map iteration order is randomized, so the selected schema can be nondeterministic when multiple media types are present. Consider preferring "application/json" explicitly when present, otherwise selecting deterministically by sorting content keys.

Suggested change
// responseSchema extracts the first JSON schema from a response's content map.
func responseSchema(resp spec.Response) *spec.Schema {
for _, mt := range resp.Content {
if mt.Schema != nil {
return mt.Schema
}
}
// responseSchema extracts a response schema from the content map.
// It prefers application/json when present, otherwise it selects
// deterministically by sorted media type.
func responseSchema(resp spec.Response) *spec.Schema {
if mt, ok := resp.Content["application/json"]; ok && mt.Schema != nil {
return mt.Schema
}
contentTypes := make([]string, 0, len(resp.Content))
for contentType := range resp.Content {
contentTypes = append(contentTypes, contentType)
}
sort.Strings(contentTypes)
for _, contentType := range contentTypes {
if mt := resp.Content[contentType]; mt.Schema != nil {
return mt.Schema
}
}

Copilot uses AI. Check for mistakes.
@disk0Dancer
Copy link
Copy Markdown
Owner

@copilot skills тоже должны быть одновлены в соответствии с изменениями в коде

также, продолжим думать над фичей, как иногда есть api c вебхуками как быть в такиз случаях, можно ил обрабатывать события (тестовые или с прода через cli) давай рассмотрим все возможные api specs и поведение чтобы написать генераторы cli и для них

как быть с пагинацией?

давай пройдем по всей документации openapi spec 3.0 и посмотримм что мы можем реализлвать уже а что еще нужно задизайнить и реализовать, также создай agents md напиши туда правила контрибьюшна (дизайнишь функцинал, пишешь доку, пишешь тесты, пишешь код, пишешь skill? прогоняешь тесты + ci? ветка комит пуш pr и тд)

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 21, 2026

@copilot skills тоже должны быть одновлены в соответствии с изменениями в коде

...

Сделал в bb003c1.

Обновил skills под новые команды (compose, mock) в skills/climate.md и skills/climate-generator/SKILL.md, добавил roadmap/дизайн по OpenAPI 3.x с разбором webhooks/event APIs и pagination в docs/openapi-3-support-matrix.md, а также добавил AGENTS.md с правилами контрибьюшна (дизайн → дока → тесты → код → skills → build/test → CI → commit/push/PR).

Также обновил README.md и docs/index.md чтобы новые возможности были отражены в основной документации.

@disk0Dancer
Copy link
Copy Markdown
Owner

@copilot давай также интегрируем mock возможность отправлять events (есть системы которые интегрируются по вебхукам,давай попробуем сделать генератор для таких ценариев (cli может отправлять события на локальный дев ендпоинт и тд)

Copilot AI and others added 7 commits April 22, 2026 08:56
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 22, 2026

@copilot давай также интегрируем mock возможность отправлять events (есть системы которые интегрируются по в�...

Сделал в ca05d2e (доп. правки: c7aaf34, d12e958, 90737c6).

Интегрировал webhook/event сценарий в climate mock: добавлены --emit-url, --event-path, --event-method для one-shot отправки синтетического события на локальный/dev endpoint. Payload генерируется из схемы операции (с приоритетом requestBody, fallback на 2xx response), запрос отправляется как JSON.

Также добавил валидации флагов и тесты (internal/mock/mock_test.go), обновил docs/skills по новому режиму.

Copilot AI changed the title feat: OpenAPI spec composition and local mock server feat: OpenAPI spec composition and local mock server with event emission Apr 22, 2026
Copilot AI requested a review from disk0Dancer April 22, 2026 09:08
@disk0Dancer disk0Dancer merged commit 6d9c663 into main Apr 22, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants