-
Notifications
You must be signed in to change notification settings - Fork 34
Expand file tree
/
Copy pathvalidator.go
More file actions
140 lines (122 loc) · 3.85 KB
/
validator.go
File metadata and controls
140 lines (122 loc) · 3.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/*
* Copyright 2025 The CNCF ModelPack Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package schema
import (
"bytes"
"encoding/json"
"fmt"
"io"
v1 "github.com/modelpack/model-spec/specs-go/v1"
"github.com/santhosh-tekuri/jsonschema/v5"
)
// Validator wraps a media type string identifier and implements validation against a JSON schema.
type Validator string
// Validate validates the given reader against the schema of the wrapped media type.
func (v Validator) Validate(src io.Reader) error {
// run the media type specific validation
if fn, ok := validateByMediaType[v]; ok {
if fn == nil {
return fmt.Errorf("internal error: mapValidate is nil for %s", string(v))
}
// buffer the src so the media type validation and the schema validation can both read it
buf, err := io.ReadAll(src)
if err != nil {
return fmt.Errorf("failed to read input: %w", err)
}
src = bytes.NewReader(buf)
err = fn(buf)
if err != nil {
return err
}
}
// json schema validation
return v.validateSchema(src)
}
func (v Validator) validateSchema(src io.Reader) error {
if _, ok := specs[v]; !ok {
return fmt.Errorf("no validator available for %s", string(v))
}
c := jsonschema.NewCompiler()
// load the schema files from the embedded FS
dir, err := specFS.ReadDir(".")
if err != nil {
return fmt.Errorf("spec embedded directory could not be loaded: %w", err)
}
for _, file := range dir {
if file.IsDir() {
continue
}
specBuf, err := specFS.ReadFile(file.Name())
if err != nil {
return fmt.Errorf("could not read spec file %s: %w", file.Name(), err)
}
err = c.AddResource(file.Name(), bytes.NewReader(specBuf))
if err != nil {
return fmt.Errorf("failed to add spec file %s: %w", file.Name(), err)
}
if len(specURLs[file.Name()]) == 0 {
// this would be a bug in the validation code itself, add any missing entry to schema.go
return fmt.Errorf("spec file has no aliases: %s", file.Name())
}
for _, specURL := range specURLs[file.Name()] {
err = c.AddResource(specURL, bytes.NewReader(specBuf))
if err != nil {
return fmt.Errorf("failed to add spec file %s as url %s: %w", file.Name(), specURL, err)
}
}
}
// compile based on the type of validator
schema, err := c.Compile(specs[v])
if err != nil {
return fmt.Errorf("failed to compile schema %s: %w", string(v), err)
}
// read in the user input and validate
var input interface{}
err = json.NewDecoder(src).Decode(&input)
if err != nil {
return fmt.Errorf("unable to parse json to validate: %w", err)
}
err = schema.Validate(input)
if err != nil {
return fmt.Errorf("validation failed: %w", err)
}
return nil
}
type validateFunc func([]byte) error
var validateByMediaType = map[Validator]validateFunc{
ValidatorMediaTypeModelConfig: validateConfig,
}
func validateConfig(buf []byte) error {
var model v1.Model
err := json.Unmarshal(buf, &model)
if err != nil {
return fmt.Errorf("invalid model structure: %w", err)
}
// Minimal structural validation for required fields
if model.Descriptor.Name == "" {
return fmt.Errorf("missing descriptor.name")
}
if len(model.ModelFS.DiffIDs) == 0 {
return fmt.Errorf("missing modelfs.diffIds")
}
// Validate digest format for each diffId
for _, d := range model.ModelFS.DiffIDs {
if err := d.Validate(); err != nil {
return fmt.Errorf("invalid diffId %q: %w", d, err)
}
}
return nil
}