feat: OpenAPI spec composition and local mock server with event emission#6
Conversation
Agent-Logs-Url: https://github.com/disk0Dancer/climate/sessions/6163d378-07f4-4b75-804c-ed5506a7c703 Co-authored-by: disk0Dancer <89835485+disk0Dancer@users.noreply.github.com>
Agent-Logs-Url: https://github.com/disk0Dancer/climate/sessions/6163d378-07f4-4b75-804c-ed5506a7c703 Co-authored-by: disk0Dancer <89835485+disk0Dancer@users.noreply.github.com>
There was a problem hiding this comment.
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/composeto load/merge multiple OpenAPI specs (path prefixing + component namespacing +$refrewriting) and exposesclimate compose. - Introduces
internal/mockto serve generated JSON responses for OpenAPI paths and exposesclimate 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.
| // 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 |
There was a problem hiding this comment.
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.
| // 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 |
| sort.Strings(paths) | ||
| for _, path := range paths { | ||
| item := s.openAPI.Paths[path] | ||
| for method := range item.Operations() { |
There was a problem hiding this comment.
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.
| 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 { |
| // Merge security schemes (last writer wins for same name). | ||
| for name, scheme := range src.Components.SecuritySchemes { | ||
| dst.Components.SecuritySchemes[name] = scheme | ||
| } |
There was a problem hiding this comment.
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.
| statusCode, body := s.generateResponse(op) | ||
| w.Header().Set("Content-Type", "application/json") | ||
| w.WriteHeader(statusCode) | ||
| if encErr := json.NewEncoder(w).Encode(body); encErr != nil { |
There was a problem hiding this comment.
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).
| 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"}`) | ||
| } |
There was a problem hiding this comment.
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.
| // 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| // 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 skills тоже должны быть одновлены в соответствии с изменениями в коде также, продолжим думать над фичей, как иногда есть api c вебхуками как быть в такиз случаях, можно ил обрабатывать события (тестовые или с прода через cli) давай рассмотрим все возможные api specs и поведение чтобы написать генераторы cli и для них как быть с пагинацией? давай пройдем по всей документации openapi spec 3.0 и посмотримм что мы можем реализлвать уже а что еще нужно задизайнить и реализовать, также создай agents md напиши туда правила контрибьюшна (дизайнишь функцинал, пишешь доку, пишешь тесты, пишешь код, пишешь skill? прогоняешь тесты + ci? ветка комит пуш pr и тд) |
… guide Agent-Logs-Url: https://github.com/disk0Dancer/climate/sessions/de38ce8b-9d81-498c-934f-97dbc7d756b5 Co-authored-by: disk0Dancer <89835485+disk0Dancer@users.noreply.github.com>
... Сделал в Обновил skills под новые команды ( Также обновил |
|
@copilot давай также интегрируем mock возможность отправлять events (есть системы которые интегрируются по вебхукам,давай попробуем сделать генератор для таких ценариев (cli может отправлять события на локальный дев ендпоинт и тд) |
Agent-Logs-Url: https://github.com/disk0Dancer/climate/sessions/93c959e7-f56b-44a4-bc4d-ab20f65bacf4 Co-authored-by: disk0Dancer <89835485+disk0Dancer@users.noreply.github.com>
Agent-Logs-Url: https://github.com/disk0Dancer/climate/sessions/93c959e7-f56b-44a4-bc4d-ab20f65bacf4 Co-authored-by: disk0Dancer <89835485+disk0Dancer@users.noreply.github.com>
Agent-Logs-Url: https://github.com/disk0Dancer/climate/sessions/93c959e7-f56b-44a4-bc4d-ab20f65bacf4 Co-authored-by: disk0Dancer <89835485+disk0Dancer@users.noreply.github.com>
…k note Agent-Logs-Url: https://github.com/disk0Dancer/climate/sessions/93c959e7-f56b-44a4-bc4d-ab20f65bacf4 Co-authored-by: disk0Dancer <89835485+disk0Dancer@users.noreply.github.com>
Agent-Logs-Url: https://github.com/disk0Dancer/climate/sessions/93c959e7-f56b-44a4-bc4d-ab20f65bacf4 Co-authored-by: disk0Dancer <89835485+disk0Dancer@users.noreply.github.com>
Agent-Logs-Url: https://github.com/disk0Dancer/climate/sessions/93c959e7-f56b-44a4-bc4d-ab20f65bacf4 Co-authored-by: disk0Dancer <89835485+disk0Dancer@users.noreply.github.com>
Agent-Logs-Url: https://github.com/disk0Dancer/climate/sessions/93c959e7-f56b-44a4-bc4d-ab20f65bacf4 Co-authored-by: disk0Dancer <89835485+disk0Dancer@users.noreply.github.com>
Сделал в Интегрировал webhook/event сценарий в Также добавил валидации флагов и тесты ( |
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 facadeMerges 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:/usersMerge logic (
internal/compose):components/schemasandcomponents/parameterswith a slug derived from the prefix (e.g./api/orders→api-orders-) to prevent cross-service collisions$refstrings in parameters, request bodies, and responses to point at the new namespaced namesclimate mock— local simulatorStarts 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.
Response generation (
internal/mock):object→ all declared properties,array→ one-element array, scalars → zero values,$ref→ resolved and generated (max depth 4){petId}) compiled to regexps; longer paths matched firstAllowheader for undefined methods, 404 for unregistered paths--latency <ms>adds uniform artificial delayEvent/webhook simulation mode
climate mocknow also supports one-shot synthetic event emission for webhook-style integrations:Event emission behavior:
--emit-urlenables emit mode (send one event and exit)--event-pathselects the OpenAPI operation used to generate payload--event-methodchooses HTTP method (validated; defaults to POST)requestBodyschema (application/jsonfirst), then falls back to first successful 2xx response schemaContent-TypeAdditional follow-up updates from feedback
skills/climate.mdskills/climate-generator/SKILL.mddocs/openapi-3-support-matrix.md(implemented vs planned support, including webhooks/event APIs and pagination strategy)AGENTS.md(design → docs → tests → code → skills → validation/CI workflow)README.mddocs/index.mdDesign docs
docs/design-compose.md— algorithm walkthrough, auth strategy, namespacing table, limitationsdocs/design-mock.md— request matching, response generation table, limitations, and event/webhook simulation modedocs/openapi-3-support-matrix.md— OpenAPI 3.x support matrix and roadmap