Skip to content

Commit 5056ae7

Browse files
feat: move reading and writing to io package, implement basic validation and add logging
1 parent 1af5d14 commit 5056ae7

File tree

10 files changed

+456
-118
lines changed

10 files changed

+456
-118
lines changed

cmd/completeness.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/spf13/cobra"
88

99
"github.com/idlab-discover/AIBoMGen-cli/internal/completeness"
10+
bomio "github.com/idlab-discover/AIBoMGen-cli/internal/io"
1011
"github.com/idlab-discover/AIBoMGen-cli/internal/metadata"
1112
)
1213

@@ -37,7 +38,7 @@ var completenessCmd = &cobra.Command{
3738
}
3839
}
3940

40-
bom, err := completeness.ReadBOM(inPath, inFormat)
41+
bom, err := bomio.ReadBOM(inPath, inFormat)
4142
if err != nil {
4243
return err
4344
}

cmd/validate.go

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,86 @@ package cmd
22

33
import (
44
"fmt"
5+
"strings"
56

7+
"github.com/idlab-discover/AIBoMGen-cli/internal/completeness"
8+
bomio "github.com/idlab-discover/AIBoMGen-cli/internal/io"
9+
"github.com/idlab-discover/AIBoMGen-cli/internal/metadata"
10+
"github.com/idlab-discover/AIBoMGen-cli/internal/validator"
611
"github.com/spf13/cobra"
712
)
813

9-
// validateCmd represents the validate command
14+
var (
15+
validateInput string
16+
validateFormat string
17+
validateStrict bool
18+
validateMinScore float64
19+
validateCheckModelCard bool
20+
validateLogLevel string
21+
)
22+
1023
var validateCmd = &cobra.Command{
1124
Use: "validate",
1225
Short: "Validate an existing AIBOM file",
1326
Long: "Validates that a CycloneDX AIBOM JSON is well-formed and optionally checks for required model card fields in strict mode.",
1427
RunE: func(cmd *cobra.Command, args []string) error {
15-
fmt.Println("Not implemented")
28+
if validateInput == "" {
29+
return fmt.Errorf("--input is required")
30+
}
31+
32+
// Resolve effective log level.
33+
level := strings.ToLower(strings.TrimSpace(validateLogLevel))
34+
if level == "" {
35+
level = "standard"
36+
}
37+
switch level {
38+
case "quiet", "standard", "debug":
39+
// ok
40+
default:
41+
return fmt.Errorf("invalid --log-level %q (expected quiet|standard|debug)", validateLogLevel)
42+
}
43+
44+
// Wire internal package logging based on log level.
45+
if level != "quiet" {
46+
lw := cmd.ErrOrStderr()
47+
validator.SetLogger(lw)
48+
if level == "debug" {
49+
metadata.SetLogger(lw)
50+
completeness.SetLogger(lw)
51+
}
52+
}
53+
54+
// Read BOM
55+
bom, err := bomio.ReadBOM(validateInput, validateFormat)
56+
if err != nil {
57+
return fmt.Errorf("failed to read BOM: %w", err)
58+
}
59+
60+
// Validate
61+
opts := validator.ValidationOptions{
62+
StrictMode: validateStrict,
63+
MinCompletenessScore: validateMinScore,
64+
CheckModelCard: validateCheckModelCard,
65+
}
66+
67+
result := validator.Validate(bom, opts)
68+
validator.PrintReport(result)
69+
70+
if !result.Valid {
71+
return fmt.Errorf("validation failed")
72+
}
73+
1674
return nil
1775
},
1876
}
1977

2078
func init() {
79+
validateCmd.Flags().StringVarP(&validateInput, "input", "i", "", "Path to AIBOM file (required)")
80+
validateCmd.Flags().StringVarP(&validateFormat, "format", "f", "auto", "Input format: json|xml|auto")
81+
validateCmd.Flags().BoolVar(&validateStrict, "strict", false, "Strict mode: fail on missing required fields")
82+
validateCmd.Flags().Float64Var(&validateMinScore, "min-score", 0.0, "Minimum completeness score (0.0-1.0)")
83+
validateCmd.Flags().BoolVar(&validateCheckModelCard, "check-model-card", true, "Validate model card fields")
84+
validateCmd.Flags().StringVar(&validateLogLevel, "log-level", "standard", "Log level: quiet|standard|debug (default: standard)")
85+
86+
validateCmd.MarkFlagRequired("input")
2187
}

internal/completeness/completeness.go

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
package completeness
22

33
import (
4-
"os"
5-
"path/filepath"
6-
"strings"
7-
84
"github.com/idlab-discover/AIBoMGen-cli/internal/metadata"
95

106
cdx "github.com/CycloneDX/cyclonedx-go"
@@ -67,33 +63,3 @@ func Check(bom *cdx.BOM) Report {
6763
MissingOptional: missingOpt,
6864
}
6965
}
70-
71-
func ReadBOM(path string, format string) (*cdx.BOM, error) {
72-
f, err := os.Open(path)
73-
if err != nil {
74-
return nil, err
75-
}
76-
defer f.Close()
77-
78-
actual := strings.ToLower(strings.TrimSpace(format))
79-
if actual == "" || actual == "auto" {
80-
if strings.EqualFold(filepath.Ext(path), ".xml") {
81-
actual = "xml"
82-
} else {
83-
actual = "json"
84-
}
85-
}
86-
87-
fileFmt := cdx.BOMFileFormatJSON
88-
if actual == "xml" {
89-
fileFmt = cdx.BOMFileFormatXML
90-
}
91-
92-
bom := new(cdx.BOM)
93-
dec := cdx.NewBOMDecoder(f, fileFmt)
94-
if err := dec.Decode(bom); err != nil {
95-
return nil, err
96-
}
97-
98-
return bom, nil
99-
}

internal/completeness/completeness_test.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"testing"
77

88
"github.com/idlab-discover/AIBoMGen-cli/internal/fetcher"
9+
bomio "github.com/idlab-discover/AIBoMGen-cli/internal/io"
910
"github.com/idlab-discover/AIBoMGen-cli/internal/metadata"
1011
"github.com/idlab-discover/AIBoMGen-cli/internal/scanner"
1112

@@ -170,7 +171,7 @@ func TestReadBOM_JSON_ExplicitFormat(t *testing.T) {
170171
p := filepath.Join(dir, "bom.json")
171172
writeBOMFile(t, p, cdx.BOMFileFormatJSON)
172173

173-
b, err := ReadBOM(p, "json")
174+
b, err := bomio.ReadBOM(p, "json")
174175
if err != nil {
175176
t.Fatalf("ReadBOM err = %v", err)
176177
}
@@ -183,7 +184,7 @@ func TestReadBOM_XML_ExplicitFormat(t *testing.T) {
183184
p := filepath.Join(dir, "bom.xml")
184185
writeBOMFile(t, p, cdx.BOMFileFormatXML)
185186

186-
b, err := ReadBOM(p, "xml")
187+
b, err := bomio.ReadBOM(p, "xml")
187188
if err != nil {
188189
t.Fatalf("ReadBOM err = %v", err)
189190
}
@@ -196,20 +197,20 @@ func TestReadBOM_AutoDetectsByExtension(t *testing.T) {
196197

197198
pJSON := filepath.Join(dir, "bom.json")
198199
writeBOMFile(t, pJSON, cdx.BOMFileFormatJSON)
199-
if _, err := ReadBOM(pJSON, "auto"); err != nil {
200+
if _, err := bomio.ReadBOM(pJSON, "auto"); err != nil {
200201
t.Fatalf("ReadBOM(json, auto) err = %v", err)
201202
}
202203

203204
pXML := filepath.Join(dir, "bom.xml")
204205
writeBOMFile(t, pXML, cdx.BOMFileFormatXML)
205-
if _, err := ReadBOM(pXML, ""); err != nil { // empty behaves like auto
206+
if _, err := bomio.ReadBOM(pXML, ""); err != nil { // empty behaves like auto
206207
t.Fatalf("ReadBOM(xml, empty) err = %v", err)
207208
}
208209
}
209210

210211
// Test ReadBOM function error cases
211212
func TestReadBOM_InvalidPath_ReturnsError(t *testing.T) {
212-
if _, err := ReadBOM("/definitely/does/not/exist/lol.json", "json"); err == nil {
213+
if _, err := bomio.ReadBOM("/definitely/does/not/exist/lol.json", "json"); err == nil {
213214
t.Fatalf("expected error for missing file")
214215
}
215216
}
@@ -222,7 +223,7 @@ func TestReadBOM_InvalidContent_ReturnsDecodeError(t *testing.T) {
222223
t.Fatalf("write file: %v", err)
223224
}
224225

225-
if _, err := ReadBOM(p, "json"); err == nil {
226+
if _, err := bomio.ReadBOM(p, "json"); err == nil {
226227
t.Fatalf("expected decode error")
227228
}
228229
}
@@ -232,7 +233,7 @@ func TestReadBOM_FormatMismatch_ReturnsDecodeError(t *testing.T) {
232233
p := filepath.Join(dir, "bom.json")
233234
writeBOMFile(t, p, cdx.BOMFileFormatJSON)
234235

235-
if _, err := ReadBOM(p, "xml"); err == nil {
236+
if _, err := bomio.ReadBOM(p, "xml"); err == nil {
236237
t.Fatalf("expected decode error when decoding json as xml")
237238
}
238239
}

internal/generator/generator.go

Lines changed: 5 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package generator
22

33
import (
44
"context"
5-
"fmt"
6-
"io"
75
"net/http"
86
"os"
97
"path/filepath"
@@ -12,6 +10,7 @@ import (
1210

1311
"github.com/idlab-discover/AIBoMGen-cli/internal/builder"
1412
"github.com/idlab-discover/AIBoMGen-cli/internal/fetcher"
13+
bomio "github.com/idlab-discover/AIBoMGen-cli/internal/io"
1514
"github.com/idlab-discover/AIBoMGen-cli/internal/scanner"
1615

1716
cdx "github.com/CycloneDX/cyclonedx-go"
@@ -22,8 +21,6 @@ type DiscoveredBOM struct {
2221
BOM *cdx.BOM
2322
}
2423

25-
var logOut io.Writer
26-
2724
// BuildPerDiscovery orchestrates: fetch HF API (optional) → build BOM per model via registry-driven builder.
2825
func BuildPerDiscovery(discoveries []scanner.Discovery, hfToken string, timeout time.Duration) ([]DiscoveredBOM, error) {
2926
results := make([]DiscoveredBOM, 0, len(discoveries))
@@ -93,74 +90,11 @@ func WriteWithFormatAndSpec(outputPath string, bom *cdx.BOM, format string, spec
9390
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
9491
return err
9592
}
96-
97-
actual := format
98-
if actual == "auto" || actual == "" {
99-
ext := strings.ToLower(filepath.Ext(outputPath))
100-
if ext == ".xml" {
101-
actual = "xml"
102-
} else {
103-
actual = "json"
104-
}
105-
}
106-
107-
// Enforce extension/format consistency when extension present and format explicitly set
108-
ext := strings.ToLower(filepath.Ext(outputPath))
109-
if actual != "auto" && ext != "" {
110-
switch actual {
111-
case "xml":
112-
if ext != ".xml" {
113-
return fmt.Errorf("output path extension %q does not match format %q", ext, actual)
114-
}
115-
case "json":
116-
if ext != ".json" {
117-
return fmt.Errorf("output path extension %q does not match format %q", ext, actual)
118-
}
119-
}
120-
}
121-
122-
fileFmt := cdx.BOMFileFormatJSON
123-
if actual == "xml" {
124-
fileFmt = cdx.BOMFileFormatXML
125-
}
126-
127-
f, err := os.Create(outputPath)
128-
if err != nil {
129-
return err
130-
}
131-
defer f.Close()
132-
133-
encoder := cdx.NewBOMEncoder(f, fileFmt)
134-
encoder.SetPretty(true)
135-
136-
if spec == "" {
137-
return encoder.Encode(bom)
138-
}
139-
140-
sv, ok := ParseSpecVersion(spec)
141-
if !ok {
142-
return fmt.Errorf("unsupported CycloneDX spec version: %q", spec)
143-
}
144-
return encoder.EncodeVersion(bom, sv)
93+
return bomio.WriteBOM(bom, outputPath, format, spec)
14594
}
14695

96+
// ParseSpecVersion parses a spec version string.
97+
// Deprecated: Use bomio.ParseSpecVersion instead.
14798
func ParseSpecVersion(s string) (cdx.SpecVersion, bool) {
148-
switch s {
149-
case "1.0":
150-
return cdx.SpecVersion1_0, true
151-
case "1.1":
152-
return cdx.SpecVersion1_1, true
153-
case "1.2":
154-
return cdx.SpecVersion1_2, true
155-
case "1.3":
156-
return cdx.SpecVersion1_3, true
157-
case "1.4":
158-
return cdx.SpecVersion1_4, true
159-
case "1.5":
160-
return cdx.SpecVersion1_5, true
161-
case "1.6":
162-
return cdx.SpecVersion1_6, true
163-
default:
164-
return 0, false
165-
}
99+
return bomio.ParseSpecVersion(s)
166100
}

0 commit comments

Comments
 (0)