Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
340c081
feat: add oq pipeline query language for OpenAPI schema graphs
vishalg0wda Mar 12, 2026
b5dc93a
style: fix gofmt formatting
vishalg0wda Mar 12, 2026
ded07af
build: add replace directive for cmd/openapi to resolve local packages
vishalg0wda Mar 12, 2026
d88cea1
fix: resolve remaining testifylint errors in test files
vishalg0wda Mar 12, 2026
dbdaafd
fix: resolve all golangci-lint errors
vishalg0wda Mar 12, 2026
c02147e
fix: guard map lookup to satisfy nil-panic linter
vishalg0wda Mar 12, 2026
26edf4a
fix: address PR review feedback
vishalg0wda Mar 12, 2026
200bdd9
feat: add new oq pipeline stages and operation fields
vishalg0wda Mar 12, 2026
df5461d
fix: use assert.Len for testifylint compliance
vishalg0wda Mar 12, 2026
9f3ba40
fix: address PR review feedback and improve test coverage
vishalg0wda Mar 12, 2026
8af8105
feat: add TOON output format for oq
vishalg0wda Mar 12, 2026
f4323f9
feat: add query-reference subcommand, oq README, and fix expr parser …
vishalg0wda Mar 12, 2026
a91d688
feat: add edge annotations, graph analysis stages, and new schema fields
vishalg0wda Mar 12, 2026
48b8cf3
refactor: swap query command arg order to query-first
vishalg0wda Mar 12, 2026
41975c1
fix: remove redundant isNull field and treat empty strings as falsy i…
vishalg0wda Mar 12, 2026
b71bcd7
refactor: split oq/oq.go into parse, exec, format, field modules
vishalg0wda Mar 12, 2026
395c19c
fix: re-trigger CI for mod-check
vishalg0wda Mar 12, 2026
de1339a
fix: remove replace directive, increase test coverage, and document m…
vishalg0wda Mar 13, 2026
cfaf308
fix: detectCycle marking non-cycle ancestors as circular, FormatToon …
vishalg0wda Mar 13, 2026
c5c2495
fix: update cmd/openapi dependency to latest commit
vishalg0wda Mar 13, 2026
e4afdd2
test: add descriptive assertion messages to oq tests
vishalg0wda Mar 13, 2026
0becad8
fix: update cmd/openapi dependency to latest commit
vishalg0wda Mar 13, 2026
e705517
fix: avoid nilaway false positive in execSample by replacing slices.C…
vishalg0wda Mar 13, 2026
06a0016
fix: update cmd/openapi dependency to latest commit
vishalg0wda Mar 13, 2026
b927a3f
feat: add jq-inspired syntax to oq query language
vishalg0wda Mar 13, 2026
4bf74e8
fix: lint issues, update docs for jq-style oq syntax
vishalg0wda Mar 13, 2026
19d9a40
Merge branch 'main' into feat/oq-query-language
vishalg0wda Mar 13, 2026
833a44d
fix: update cmd/openapi dependency to latest commit
vishalg0wda Mar 13, 2026
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
24 changes: 24 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,30 @@ git commit -m "feat: implement prefixEncoding and itemEncoding for OpenAPI 3.2
3. **Searchability**: Easier to search and filter commits
4. **Tool Compatibility**: Works better with automated tools and scripts

## Multi-Module Dependency Management

This repository uses Go workspaces (`go.work`) with multiple modules. The `cmd/openapi` module depends on the root `github.com/speakeasy-api/openapi` module.

### How Local Development Works

The `go.work` file lists all modules, so during local development the workspace resolves cross-module imports automatically. You do **not** need a `replace` directive in `cmd/openapi/go.mod`.

### When Adding New Packages to the Root Module

If you add new packages to the root module (e.g., `oq/`, `graph/`) that `cmd/openapi` imports, the published module version won't contain them yet. The workspace handles this locally, but `cmd/openapi/go.mod` must reference a version that includes the new packages for CI to pass `mod-check`.

**Do NOT use `replace` directives.** Instead:

1. Push your branch with the new root module packages.
2. From the repo root, update `cmd/openapi` to reference your branch commit:
```bash
GOWORK=off go get -C cmd/openapi github.com/speakeasy-api/openapi@<commit-sha>
GOWORK=off go mod tidy -C cmd/openapi
```
3. Verify with `mise run mod-check`.

This gives `cmd/openapi/go.mod` a pseudo-version (e.g., `v1.19.6-0.20260312183335-395c19cd8edd`) that resolves correctly both locally and in CI. Each subsequent push that changes the root module requires repeating step 2 with the new commit SHA.

## Linter Rules

This project uses `golangci-lint` with strict rules. Run `mise lint` to check. The most common violations are listed below. **When you encounter a new common lint pattern not documented here, add it to this section so future sessions avoid the same mistakes.**
Expand Down
182 changes: 182 additions & 0 deletions cmd/openapi/commands/openapi/query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package openapi

import (
"context"
"errors"
"fmt"
"os"

"github.com/speakeasy-api/openapi/graph"
"github.com/speakeasy-api/openapi/openapi"
"github.com/speakeasy-api/openapi/oq"
"github.com/speakeasy-api/openapi/references"
"github.com/spf13/cobra"
)

var queryCmd = &cobra.Command{
Use: "query <query> [input-file]",
Short: "Query an OpenAPI specification using the oq pipeline language",
Long: `Query an OpenAPI specification using the oq pipeline language to answer
structural and semantic questions about schemas and operations.

The query argument comes first, followed by an optional input file. If no file
is given, reads from stdin.

Examples:
# Deeply nested components (jq-style syntax)
openapi spec query 'schemas.components | sort_by(depth; desc) | first(10) | pick name, depth' petstore.yaml

# Pipe from stdin
cat spec.yaml | openapi spec query 'schemas | count'

# Explicit stdin
openapi spec query 'schemas | count' -

# Filter with select()
openapi spec query 'schemas | select(union_width > 0) | sort_by(union_width; desc) | first(10)' petstore.yaml

# Dead components (no incoming references)
openapi spec query 'schemas.components | select(in_degree == 0) | pick name' petstore.yaml

# Variable binding — exclude seed from reachable results
openapi spec query 'schemas | select(name == "Pet") | let $pet = name | reachable | select(name != $pet)' petstore.yaml

# User-defined functions
openapi spec query 'def hot: select(in_degree > 5); schemas.components | hot | pick name' petstore.yaml

# Alternative operator — fallback for null/falsy values
openapi spec query 'schemas | select(name // "none" != "none")' petstore.yaml

# If-then-else conditional
openapi spec query 'schemas | select(if is_component then depth > 3 else true end)' petstore.yaml

# Blast radius
openapi spec query 'schemas.components | select(name == "Error") | blast-radius | length' petstore.yaml

# Explain a query plan
openapi spec query 'schemas.components | select(depth > 5) | sort_by(depth; desc) | explain' petstore.yaml

Pipeline stages (jq-style):
Source: schemas, schemas.components, schemas.inline, operations
Traversal: refs-out, refs-in, reachable, ancestors, properties, union-members, items,
ops, schemas, path(A; B), connected, blast-radius, neighbors(N)
Analysis: orphans, leaves, cycles, clusters, tag-boundary, shared-refs
Filter: select(expr), pick <fields>, sort_by(field; desc), first(N), last(N),
sample(N), top(N; field), bottom(N; field), unique, group_by(field), length
Variables: let $var = expr
Functions: def name: body; def name($p): body; include "file.oq";
Meta: explain, fields, format(table|json|markdown|toon)

Legacy syntax (where, sort, take, head, select fields, group-by, count) is still supported.

Expression operators: ==, !=, >, <, >=, <=, and, or, not, //, has(), matches,
if-then-else-end, string interpolation \(expr)`,
Args: queryArgs(),
Run: runQuery,
}

var queryOutputFormat string
var queryFromFile string

func init() {
queryCmd.Flags().StringVar(&queryOutputFormat, "format", "table", "output format: table, json, markdown, or toon")
queryCmd.Flags().StringVarP(&queryFromFile, "file", "f", "", "read query from file instead of argument")
}

func runQuery(cmd *cobra.Command, args []string) {
ctx := cmd.Context()

// args[0] = query (or input file if using -f), args[1] = input file (optional)
queryStr := ""
inputFile := "-" // default to stdin

if queryFromFile != "" {
data, err := os.ReadFile(queryFromFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading query file: %v\n", err)
os.Exit(1)
}
queryStr = string(data)
// When using -f, all positional args are input files
if len(args) > 0 {
inputFile = args[0]
}
} else if len(args) >= 1 {
queryStr = args[0]
if len(args) >= 2 {
inputFile = args[1]
}
}

if queryStr == "" {
fmt.Fprintf(os.Stderr, "Error: no query provided\n")
os.Exit(1)
}

processor, err := NewOpenAPIProcessor(inputFile, "", false)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

if err := queryOpenAPI(ctx, processor, queryStr); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

func queryOpenAPI(ctx context.Context, processor *OpenAPIProcessor, queryStr string) error {
doc, _, err := processor.LoadDocument(ctx)
if err != nil {
return err
}
if doc == nil {
return errors.New("failed to parse OpenAPI document: document is nil")
}

// Build index
idx := buildIndex(ctx, doc)

// Build graph
g := graph.Build(ctx, idx)

// Execute query
result, err := oq.Execute(queryStr, g)
if err != nil {
return fmt.Errorf("query error: %w", err)
}

// Format and output — inline format stage overrides CLI flag
format := queryOutputFormat
if result.FormatHint != "" {
format = result.FormatHint
}

var output string
switch format {
case "json":
output = oq.FormatJSON(result, g)
case "markdown":
output = oq.FormatMarkdown(result, g)
case "toon":
output = oq.FormatToon(result, g)
default:
output = oq.FormatTable(result, g)
}

fmt.Fprint(processor.stdout(), output)
if result.IsCount {
fmt.Fprintln(processor.stdout())
}

return nil
}

func buildIndex(ctx context.Context, doc *openapi.OpenAPI) *openapi.Index {
resolveOpts := references.ResolveOptions{
RootDocument: doc,
TargetDocument: doc,
TargetLocation: ".",
}
return openapi.BuildIndex(ctx, doc, resolveOpts)
}
Comment on lines +175 to +182
Copy link
Contributor

Choose a reason for hiding this comment

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

🚩 TargetLocation set to '.' instead of actual file path

In cmd/openapi/commands/openapi/query.go:140, TargetLocation is set to "." rather than the actual input file path. Every other call to BuildIndex in the codebase uses a meaningful file path (e.g., "test.yaml", "testdata/petstore.yaml"). The TargetLocation is used by isFromMainDocument() to compare against the current document stack. For single-file specs this works, but for multi-file specs with external $refs, using "." could cause isFromMainDocument() to behave incorrectly (e.g., classifying external schemas as main-document schemas). The graph test at graph/graph_test.go:29 correctly uses the actual path.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Loading
Loading