Skip to content

Commit 9f5bb21

Browse files
authored
updated web schema layout & linking (#164)
Signed-off-by: Eddie Knight <knight@linux.com>
1 parent 43688b2 commit 9f5bb21

File tree

13 files changed

+1037
-1549
lines changed

13 files changed

+1037
-1549
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ openapi.yaml
33
cmd/*/cue2openapi
44
cmd/*/openapi2md
55
docs/_site/
6+
.DS_Store

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ cuegen:
2424

2525
genopenapi:
2626
@echo " > Converting CUE schema to OpenAPI ..."
27-
@cd cmd/cue2openapi && go run . -schema ../../spec/schema.cue -output ../../openapi.yaml
27+
@cd cmd/cue2openapi && go run . -schema ../../spec -output ../../openapi.yaml -root SecurityInsights -version "$$(cat ../../VERSION)"
2828
@echo " > OpenAPI schema generation complete!"
2929

3030
genindex:
@@ -43,7 +43,7 @@ genindex:
4343

4444
gendocs: genopenapi
4545
@echo " > Generating markdown from OpenAPI ..."
46-
@cd cmd/openapi2md && go run . -input ../../openapi.yaml -output ../../spec
46+
@cd cmd/openapi2md && go run . -input ../../openapi.yaml -output ../../spec -roots SecurityInsights
4747
@echo " > Copying schema.md to docs/ for website ..."
4848
@{ \
4949
echo "---"; \

cmd/cue2openapi/converter.go

Lines changed: 128 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11
package main
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"os"
7+
"path/filepath"
8+
"sort"
69
"strings"
710

811
"cuelang.org/go/cue/ast"
12+
"cuelang.org/go/cue/load"
913
"cuelang.org/go/cue/token"
10-
"gopkg.in/yaml.v3"
14+
"github.com/goccy/go-yaml"
1115
)
1216

17+
// ConvertOpts configures CUE-to-OpenAPI conversion.
18+
type ConvertOpts struct {
19+
ManifestPath string // If set, write schema→file manifest JSON here
20+
Root string // Optional #Name whose comment sets Info.Description
21+
Version string // Override version (default: VERSION file or "unknown")
22+
Title string // OpenAPI info title (default: "Security Insights")
23+
}
24+
1325
type OpenAPISpec struct {
1426
OpenAPI string `yaml:"openapi" json:"openapi"`
1527
Info OpenAPIInfo `yaml:"info" json:"info"`
@@ -37,64 +49,146 @@ type SchemaInfo struct {
3749
Ref string `yaml:"$ref,omitempty" json:"$ref,omitempty"`
3850
}
3951

40-
func readVersion() string {
41-
path := "../../VERSION"
42-
52+
func readVersion(schemaDir string) string {
53+
path := filepath.Join(schemaDir, "VERSION")
4354
if data, err := os.ReadFile(path); err == nil {
4455
version := strings.TrimSpace(string(data))
4556
if version != "" {
4657
return version
4758
}
4859
}
49-
50-
return "unknown version"
60+
return "unknown"
5161
}
5262

53-
func parseCUEToOpenAPI(file *ast.File) *OpenAPISpec {
54-
version := readVersion()
63+
func convertCUEToOpenAPI(schemaDir, outputPath string, opts ConvertOpts) error {
64+
if !filepath.IsAbs(schemaDir) {
65+
wd, err := os.Getwd()
66+
if err != nil {
67+
return fmt.Errorf("failed to get working directory: %w", err)
68+
}
69+
schemaDir = filepath.Join(wd, schemaDir)
70+
}
71+
72+
insts := load.Instances([]string{"."}, &load.Config{Dir: schemaDir})
73+
if len(insts) == 0 || insts[0].Err != nil {
74+
err := error(nil)
75+
if len(insts) > 0 {
76+
err = insts[0].Err
77+
}
78+
return fmt.Errorf("failed to load CUE package: %v", err)
79+
}
80+
81+
version := opts.Version
82+
if version == "" {
83+
version = readVersion(schemaDir)
84+
}
85+
title := opts.Title
86+
if title == "" {
87+
title = "Security Insights"
88+
}
89+
5590
spec := &OpenAPISpec{
5691
OpenAPI: "3.0.3",
5792
Info: OpenAPIInfo{
58-
Title: "Security Insights Specification",
59-
Version: version,
93+
Title: title,
94+
Version: version,
95+
Description: "Security Insights schema definitions",
6096
},
6197
Components: OpenAPIComponents{
6298
Schemas: make(map[string]interface{}),
6399
},
64100
}
65101

66-
// Extract root type description
67-
var rootDescription string
102+
seen := make(map[string]bool)
103+
manifest := make(map[string][]string) // filename → schema names
104+
files := insts[0].Files
105+
names := make([]string, 0, len(files))
106+
byName := make(map[string]*ast.File)
107+
for _, f := range files {
108+
name := "<no filename>"
109+
if f.Filename != "" {
110+
name = filepath.Base(f.Filename)
111+
}
112+
names = append(names, name)
113+
byName[name] = f
114+
}
115+
sort.Strings(names)
116+
117+
for _, name := range names {
118+
if name == "<no filename>" {
119+
continue
120+
}
121+
f := byName[name]
122+
rootDesc, typeNames := parseFile(f, spec, seen, opts.Root)
123+
if rootDesc != "" && opts.Root != "" {
124+
spec.Info.Description = rootDesc
125+
}
126+
if len(typeNames) > 0 {
127+
manifest[name] = typeNames
128+
}
129+
}
130+
131+
if err := writeOpenAPISpec(spec, outputPath); err != nil {
132+
return err
133+
}
134+
if opts.ManifestPath != "" {
135+
if err := writeManifest(manifest, opts.ManifestPath); err != nil {
136+
return err
137+
}
138+
}
139+
return nil
140+
}
141+
142+
func writeManifest(manifest map[string][]string, path string) error {
143+
keys := make([]string, 0, len(manifest))
144+
for k := range manifest {
145+
keys = append(keys, k)
146+
}
147+
sort.Strings(keys)
148+
ordered := make(map[string][]string, len(manifest))
149+
for _, k := range keys {
150+
ordered[k] = manifest[k]
151+
}
152+
data, err := json.MarshalIndent(ordered, "", " ")
153+
if err != nil {
154+
return fmt.Errorf("marshal manifest: %w", err)
155+
}
156+
return os.WriteFile(path, data, 0644)
157+
}
158+
159+
// parseFile processes a single CUE file and merges definitions into spec.
160+
// It returns the root type description (if opts.Root matches) and the list of type names added.
161+
func parseFile(file *ast.File, spec *OpenAPISpec, seen map[string]bool, rootName string) (rootDescription string, typeNames []string) {
68162
for _, decl := range file.Decls {
69-
if field, ok := decl.(*ast.Field); ok {
70-
if ident, ok := field.Label.(*ast.Ident); ok && ident.Name == "#SecurityInsights" {
71-
if field.Comments() != nil {
72-
for _, cg := range field.Comments() {
73-
for _, c := range cg.List {
74-
if c.Text != "" {
75-
rootDescription = extractComment(c.Text)
76-
break
77-
}
163+
field, ok := decl.(*ast.Field)
164+
if !ok {
165+
continue
166+
}
167+
ident, ok := field.Label.(*ast.Ident)
168+
if !ok || !strings.HasPrefix(ident.Name, "#") {
169+
continue
170+
}
171+
typeName := strings.TrimPrefix(ident.Name, "#")
172+
if rootName != "" && ident.Name == "#"+rootName {
173+
if field.Comments() != nil {
174+
for _, cg := range field.Comments() {
175+
for _, c := range cg.List {
176+
if c.Text != "" {
177+
rootDescription = extractComment(c.Text)
178+
break
78179
}
79180
}
80181
}
81-
if rootDescription != "" {
82-
spec.Info.Description = rootDescription
83-
}
84-
break
85182
}
86183
}
87-
}
88-
89-
// Walk through all declarations to find type definitions
90-
for _, decl := range file.Decls {
91-
switch x := decl.(type) {
92-
case *ast.Field:
93-
parseDefinitionField(x, spec)
184+
if seen[typeName] {
185+
continue
94186
}
187+
seen[typeName] = true
188+
parseDefinitionField(field, spec)
189+
typeNames = append(typeNames, typeName)
95190
}
96-
97-
return spec
191+
return rootDescription, typeNames
98192
}
99193

100194
func parseDefinitionField(field *ast.Field, spec *OpenAPISpec) {

cmd/cue2openapi/go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.23.0
44

55
require (
66
cuelang.org/go v0.7.0
7-
gopkg.in/yaml.v3 v3.0.1
7+
github.com/goccy/go-yaml v1.19.2
88
)
99

1010
require (
@@ -19,4 +19,5 @@ require (
1919
github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 // indirect
2020
golang.org/x/net v0.38.0 // indirect
2121
golang.org/x/text v0.23.0 // indirect
22+
gopkg.in/yaml.v3 v3.0.1 // indirect
2223
)

cmd/cue2openapi/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ github.com/emicklei/proto v1.10.0 h1:pDGyFRVV5RvV+nkBK9iy3q67FBy9Xa7vwrOTE+g5aGw
88
github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
99
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
1010
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
11+
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
12+
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
1113
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
1214
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
1315
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=

cmd/cue2openapi/main.go

Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,49 +4,26 @@ import (
44
"flag"
55
"fmt"
66
"os"
7-
"path/filepath"
8-
9-
"cuelang.org/go/cue/load"
107
)
118

129
func main() {
13-
schemaPath := flag.String("schema", "spec/schema.cue", "Path to the CUE schema file")
10+
schemaDir := flag.String("schema", "../..", "Path to the CUE package directory")
1411
outputPath := flag.String("output", "openapi.yaml", "Output path for OpenAPI schema")
12+
manifestPath := flag.String("manifest", "", "Optional path to write schema→file manifest JSON")
13+
root := flag.String("root", "", "Optional root definition (#Name) whose comment sets spec description")
14+
version := flag.String("version", "", "Optional version string (default: VERSION file in schema dir or \"unknown\")")
15+
title := flag.String("title", "Security Insights", "OpenAPI info title")
1516
flag.Parse()
1617

17-
if err := convertCUEToOpenAPI(*schemaPath, *outputPath); err != nil {
18+
if err := convertCUEToOpenAPI(*schemaDir, *outputPath, ConvertOpts{
19+
ManifestPath: *manifestPath,
20+
Root: *root,
21+
Version: *version,
22+
Title: *title,
23+
}); err != nil {
1824
fmt.Fprintf(os.Stderr, "Error converting CUE to OpenAPI: %v\n", err)
1925
os.Exit(1)
2026
}
2127

2228
fmt.Printf("OpenAPI schema generated successfully at %s\n", *outputPath)
2329
}
24-
25-
func convertCUEToOpenAPI(schemaPath, outputPath string) error {
26-
// Resolve to absolute path if relative
27-
if !filepath.IsAbs(schemaPath) {
28-
wd, err := os.Getwd()
29-
if err != nil {
30-
return fmt.Errorf("failed to get working directory: %v", err)
31-
}
32-
schemaPath = filepath.Join(wd, schemaPath)
33-
}
34-
35-
// Load and parse the CUE schema
36-
dir := filepath.Dir(schemaPath)
37-
insts := load.Instances([]string{filepath.Base(schemaPath)}, &load.Config{
38-
Dir: dir,
39-
})
40-
41-
if len(insts) == 0 || insts[0].Err != nil {
42-
return fmt.Errorf("failed to load CUE schema: %v", insts[0].Err)
43-
}
44-
45-
// Use runtime-based conversion
46-
file := insts[0].Files[0]
47-
openapiSpec := parseCUEToOpenAPI(file)
48-
49-
// Write OpenAPI spec as YAML
50-
return writeOpenAPISpec(openapiSpec, outputPath)
51-
}
52-

cmd/openapi2md/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module github.com/ossf/security-insights/cmd/openapi2md
22

3-
go 1.21
3+
go 1.21.0
44

5-
require gopkg.in/yaml.v3 v3.0.1
5+
require github.com/goccy/go-yaml v1.19.2

cmd/openapi2md/go.sum

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
1-
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
2-
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
3-
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
4-
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
1+
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
2+
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=

0 commit comments

Comments
 (0)