Skip to content

Commit e43074e

Browse files
fix: address review feedback (revert Go bump, add tests, update docs, neutralize comment)
1 parent 3e289ec commit e43074e

7 files changed

Lines changed: 111 additions & 10 deletions

File tree

cmd/openapi/commands/openapi/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1260,7 +1260,7 @@ openapi spec query --format json 'schemas | where(isComponent) | take(5)' ./spec
12601260

12611261
| Flag | Short | Description |
12621262
| ---------- | ----- | --------------------------------------------------------------- |
1263-
| `--format` | | Output format: `table` (default), `json`, `markdown`, or `toon` |
1263+
| `--format` | | Output format: `table` (default), `json`, `markdown`, `toon`, or `gcf` |
12641264
| `--file` | `-f` | Read query from file instead of argument |
12651265

12661266
For the full query language reference, run `openapi spec query-reference`.

cmd/openapi/commands/openapi/query.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Pipeline stages:
3636
group-by(field), group-by(field, name_field), length
3737
Variables: let $var = expr
3838
Functions: def name: body; def name($p): body; include "file.oq";
39-
Output: to-yaml, format(table|json|markdown|toon)
39+
Output: to-yaml, format(table|json|markdown|toon|gcf)
4040
Meta: explain, fields
4141
4242
Operators: ==, !=, >, <, >=, <=, and, or, not, // (or default), has(),

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
module github.com/speakeasy-api/openapi
22

3-
go 1.26.1
3+
go 1.25.0
44

55
require (
6-
github.com/blackwell-systems/gcf-go v1.2.1
6+
github.com/blackwell-systems/gcf-go v1.2.2
77
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
88
github.com/speakeasy-api/jsonpath v0.6.3
99
github.com/stretchr/testify v1.11.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
github.com/blackwell-systems/gcf-go v1.2.1 h1:NsoGZKQxT8O3WKl+QssiFdvhPM0cCCyJ3a3x1H5fGDI=
2-
github.com/blackwell-systems/gcf-go v1.2.1/go.mod h1:sROSBELq+iEDf3pD1x4AcY2QccFd1KHsGajFzjVlCNc=
1+
github.com/blackwell-systems/gcf-go v1.2.2 h1:oPc9VbC5DDMPek/RMWO3zVcImVr3QTOcCIgBOLcjNRk=
2+
github.com/blackwell-systems/gcf-go v1.2.2/go.mod h1:E4fW1kxdrIoWxlI4iwZL8mh7BvdLTkE88NyijtGGcZc=
33
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
44
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
55
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

oq/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ Navigate into the internal structure of operations. These stages produce new row
113113
|-------|-------------|
114114
| `explain` | Print query plan |
115115
| `fields` | List available fields |
116-
| `format(fmt)` | Set output format (table/json/markdown/toon) |
116+
| `format(fmt)` | Set output format (table/json/markdown/toon/gcf) |
117117
| `to-yaml` | Output raw YAML nodes from underlying spec objects |
118118

119119
The `to-yaml` stage uses `path` (JSON pointer) as the wrapper key for each emitted node, giving full attribution to the source location in the spec.
@@ -349,6 +349,7 @@ openapi spec query 'schemas | take(5) | format(markdown)' spec.yaml
349349
| `json` | JSON array |
350350
| `markdown` | Markdown table |
351351
| `toon` | [TOON](https://github.com/toon-format/toon) tabular format |
352+
| `gcf` | [GCF](https://gcformat.com) pipe-delimited format with inline schemas |
352353

353354
## Examples
354355

oq/format.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,7 @@ func FormatToon(result *Result, g *graph.SchemaGraph) string {
223223
}
224224

225225
// FormatGCF formats a result using GCF (Graph Compact Format).
226-
// GCF uses positional pipe-delimited rows with inline schemas, achieving
227-
// higher LLM comprehension accuracy than TOON or JSON (90.7% vs 68.5% vs 53.6%).
226+
// GCF uses positional pipe-delimited rows with inline schemas.
228227
// See https://gcformat.com
229228
func FormatGCF(result *Result, g *graph.SchemaGraph) string {
230229
if result.Explain != "" {
@@ -399,7 +398,28 @@ func toonValue(v expr.Value) string {
399398
case expr.KindBool:
400399
return strconv.FormatBool(v.Bool)
401400
case expr.KindArray:
402-
return toonEscape(strings.Join(v.Arr, ";"))
401+
hasSemicolon := false
402+
for _, s := range v.Arr {
403+
if strings.Contains(s, ";") {
404+
hasSemicolon = true
405+
break
406+
}
407+
}
408+
if !hasSemicolon {
409+
return toonEscape(strings.Join(v.Arr, ";"))
410+
}
411+
// Fall back to JSON array syntax when elements contain ";"
412+
// to avoid ambiguity with the semicolon delimiter.
413+
var sb strings.Builder
414+
sb.WriteByte('[')
415+
for i, s := range v.Arr {
416+
if i > 0 {
417+
sb.WriteByte(',')
418+
}
419+
fmt.Fprintf(&sb, "%q", s)
420+
}
421+
sb.WriteByte(']')
422+
return sb.String()
403423
default:
404424
return "null"
405425
}

oq/oq_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,6 +1412,86 @@ func TestFormatToon_Explain(t *testing.T) {
14121412
assert.Contains(t, toon, "Source: schemas", "toon should render explain output")
14131413
}
14141414

1415+
func TestFormatGCF_Success(t *testing.T) {
1416+
t.Parallel()
1417+
g := loadTestGraph(t)
1418+
1419+
result, err := oq.Execute("schemas | where(isComponent) | take(3) | select name, type", g)
1420+
require.NoError(t, err)
1421+
1422+
out := oq.FormatGCF(result, g)
1423+
assert.Contains(t, out, "GCF profile=generic", "gcf should have profile header")
1424+
assert.Contains(t, out, "{name,type}", "gcf should declare field names")
1425+
assert.Contains(t, out, "object", "gcf should include object type value")
1426+
}
1427+
1428+
func TestFormatGCF_Count_Success(t *testing.T) {
1429+
t.Parallel()
1430+
g := loadTestGraph(t)
1431+
1432+
result, err := oq.Execute("schemas | length", g)
1433+
require.NoError(t, err)
1434+
1435+
out := oq.FormatGCF(result, g)
1436+
assert.Contains(t, out, "count=", "gcf count should use key=value format")
1437+
}
1438+
1439+
func TestFormatGCF_Groups_Success(t *testing.T) {
1440+
t.Parallel()
1441+
g := loadTestGraph(t)
1442+
1443+
result, err := oq.Execute("schemas | where(isComponent) | group-by(type)", g)
1444+
require.NoError(t, err)
1445+
1446+
out := oq.FormatGCF(result, g)
1447+
assert.Contains(t, out, "GCF profile=generic", "gcf should have profile header")
1448+
assert.Contains(t, out, "{count,key,names}", "gcf should declare group fields")
1449+
}
1450+
1451+
func TestFormatGCF_Empty_Success(t *testing.T) {
1452+
t.Parallel()
1453+
g := loadTestGraph(t)
1454+
1455+
result, err := oq.Execute(`schemas | where(isComponent) | where(name == "NonExistent")`, g)
1456+
require.NoError(t, err)
1457+
1458+
out := oq.FormatGCF(result, g)
1459+
assert.Contains(t, out, "GCF profile=generic", "empty gcf should still have profile header")
1460+
}
1461+
1462+
func TestFormatGCF_SpecialChars(t *testing.T) {
1463+
t.Parallel()
1464+
g := loadTestGraph(t)
1465+
1466+
result, err := oq.Execute("schemas | where(isComponent) | take(1) | select name, depth, isComponent", g)
1467+
require.NoError(t, err)
1468+
1469+
out := oq.FormatGCF(result, g)
1470+
assert.NotEmpty(t, out, "gcf output should not be empty")
1471+
assert.Contains(t, out, "GCF profile=generic", "gcf should have profile header")
1472+
}
1473+
1474+
func TestFormatGCF_Explain(t *testing.T) {
1475+
t.Parallel()
1476+
g := loadTestGraph(t)
1477+
1478+
result, err := oq.Execute("schemas | where(depth > 0) | explain", g)
1479+
require.NoError(t, err)
1480+
1481+
out := oq.FormatGCF(result, g)
1482+
assert.Contains(t, out, "Source: schemas", "gcf should render explain output as-is")
1483+
}
1484+
1485+
func TestFormatGCF_InlinePipeline(t *testing.T) {
1486+
t.Parallel()
1487+
g := loadTestGraph(t)
1488+
1489+
result, err := oq.Execute("schemas | where(isComponent) | take(3) | format(gcf)", g)
1490+
require.NoError(t, err)
1491+
1492+
assert.Equal(t, "gcf", result.FormatHint, "format(gcf) should set FormatHint")
1493+
}
1494+
14151495
func TestFormatMarkdown_Explain(t *testing.T) {
14161496
t.Parallel()
14171497
g := loadTestGraph(t)

0 commit comments

Comments
 (0)