Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit125cd11

Browse files
committed
jsonschema: pre-compile regexps
This is the first CL that deals with the process of preparing a schema for validation.Perform some basic checks on the scheme. Along the way, compile regexps and storethem in the schema for use during validation.Change-Id: I0e5e6ce28656dcecb6d2d4b2fdc98998fa05b6f1Reviewed-on:https://go-review.googlesource.com/c/tools/+/669696LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>Reviewed-by: Alan Donovan <adonovan@google.com>
1 parent2f18550 commit125cd11

File tree

8 files changed

+153
-34
lines changed

8 files changed

+153
-34
lines changed

‎internal/mcp/internal/jsonschema/infer_test.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"testing"
99

1010
"github.com/google/go-cmp/cmp"
11+
"github.com/google/go-cmp/cmp/cmpopts"
1112
"golang.org/x/tools/internal/mcp/internal/jsonschema"
1213
)
1314

@@ -63,7 +64,7 @@ func TestForType(t *testing.T) {
6364

6465
for_,test:=rangetests {
6566
t.Run(test.name,func(t*testing.T) {
66-
ifdiff:=cmp.Diff(test.want,test.got);diff!="" {
67+
ifdiff:=cmp.Diff(test.want,test.got,cmpopts.IgnoreUnexported(jsonschema.Schema{}));diff!="" {
6768
t.Errorf("ForType mismatch (-want +got):\n%s",diff)
6869
}
6970
})
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// This file deals with preparing a schema for validation, including various checks,
6+
// optimizations, and the resolution of cross-schema references.
7+
8+
package jsonschema
9+
10+
import (
11+
"errors"
12+
"fmt"
13+
"regexp"
14+
)
15+
16+
// A Resolved consists of a [Schema] along with associated information needed to
17+
// validate documents against it.
18+
// A Resolved has been validated against its meta-schema, and all its references
19+
// (the $ref and $dynamicRef keywords) have been resolved to their referenced Schemas.
20+
// Call [Schema.Resolve] to obtain a Resolved from a Schema.
21+
typeResolvedstruct {
22+
root*Schema
23+
}
24+
25+
// Resolve resolves all references within the schema and performs other tasks that
26+
// prepare the schema for validation.
27+
func (root*Schema)Resolve() (*Resolved,error) {
28+
// There are three steps involved in preparing a schema to validate.
29+
// 1. Check: validate the schema against a meta-schema, and perform other well-formedness
30+
// checks. Precompute some values along the way.
31+
// 2. Resolve URIs: TODO.
32+
// 3. Resolve references: TODO.
33+
iferr:=root.check();err!=nil {
34+
returnnil,err
35+
}
36+
return&Resolved{root:root},nil
37+
}
38+
39+
func (s*Schema)check()error {
40+
ifs==nil {
41+
returnerrors.New("nil schema")
42+
}
43+
varerrs []error
44+
report:=func(errerror) {errs=append(errs,err) }
45+
46+
forss:=ranges.all() {
47+
ss.checkLocal(report)
48+
}
49+
returnerrors.Join(errs...)
50+
}
51+
52+
// checkLocal checks s for validity, independently of other schemas it may refer to.
53+
// Since checking a regexp involves compiling it, checkLocal saves those compiled regexps
54+
// in the schema for later use.
55+
// It appends the errors it finds to errs.
56+
func (s*Schema)checkLocal(reportfunc(error)) {
57+
addf:=func(formatstring,args...any) {
58+
report(fmt.Errorf("jsonschema.Schema: "+format,args...))
59+
}
60+
61+
ifs==nil {
62+
addf("nil subschema")
63+
return
64+
}
65+
iferr:=s.basicChecks();err!=nil {
66+
report(err)
67+
return
68+
}
69+
70+
// TODO: validate the schema's properties,
71+
// ideally by jsonschema-validating it against the meta-schema.
72+
73+
// Check and compile regexps.
74+
ifs.Pattern!="" {
75+
re,err:=regexp.Compile(s.Pattern)
76+
iferr!=nil {
77+
addf("pattern: %w",err)
78+
}else {
79+
s.pattern=re
80+
}
81+
}
82+
iflen(s.PatternProperties)>0 {
83+
s.patternProperties=map[*regexp.Regexp]*Schema{}
84+
forreString,subschema:=ranges.PatternProperties {
85+
re,err:=regexp.Compile(reString)
86+
iferr!=nil {
87+
addf("patternProperties[%q]: %w",reString,err)
88+
continue
89+
}
90+
s.patternProperties[re]=subschema
91+
}
92+
}
93+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package jsonschema
6+
7+
import (
8+
"regexp"
9+
"testing"
10+
)
11+
12+
funcTestCheckLocal(t*testing.T) {
13+
for_,tt:=range []struct {
14+
s*Schema
15+
wantstring// error must be non-nil and match this regexp
16+
}{
17+
{nil,"nil"},
18+
{
19+
&Schema{Pattern:"]["},
20+
"regexp",
21+
},
22+
{
23+
&Schema{PatternProperties:map[string]*Schema{"*":nil}},
24+
"regexp",
25+
},
26+
} {
27+
_,err:=tt.s.Resolve()
28+
iferr==nil {
29+
t.Errorf("%s: unexpectedly passed",tt.s.json())
30+
continue
31+
}
32+
if!regexp.MustCompile(tt.want).MatchString(err.Error()) {
33+
t.Errorf("%s: did not match\nerror: %s\nregexp: %s",
34+
tt.s.json(),err,tt.want)
35+
}
36+
}
37+
}

‎internal/mcp/internal/jsonschema/schema.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"fmt"
1414
"iter"
1515
"math"
16+
"regexp"
1617
)
1718

1819
// A Schema is a JSON schema object.
@@ -106,6 +107,10 @@ type Schema struct {
106107
Then*Schema`json:"then,omitempty"`
107108
Else*Schema`json:"else,omitempty"`
108109
DependentSchemasmap[string]*Schema`json:"dependentSchemas,omitempty"`
110+
111+
// computed fields
112+
pattern*regexp.Regexp
113+
patternPropertiesmap[*regexp.Regexp]*Schema
109114
}
110115

111116
// String returns a short description of the schema.

‎internal/mcp/internal/jsonschema/validate.go‎

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"math"
1111
"math/big"
1212
"reflect"
13-
"regexp"
1413
"slices"
1514
"strings"
1615
"unicode/utf8"
@@ -19,16 +18,9 @@ import (
1918
// The value of the "$schema" keyword for the version that we can validate.
2019
constdraft202012="https://json-schema.org/draft/2020-12/schema"
2120

22-
// Temporary definition of ResolvedSchema.
23-
// The full definition deals with references between schemas, specifically the $id, $anchor and $ref keywords.
24-
// We'll ignore that for now.
25-
typeResolvedSchemastruct {
26-
root*Schema
27-
}
28-
2921
// Validate validates the instance, which must be a JSON value, against the schema.
3022
// It returns nil if validation is successful or an error if it is not.
31-
func (rs*ResolvedSchema)Validate(instanceany)error {
23+
func (rs*Resolved)Validate(instanceany)error {
3224
ifs:=rs.root.Schema;s!=""&&s!=draft202012 {
3325
returnfmt.Errorf("cannot validate version %s, only %s",s,draft202012)
3426
}
@@ -39,7 +31,7 @@ func (rs *ResolvedSchema) Validate(instance any) error {
3931

4032
// state is the state of single call to ResolvedSchema.Validate.
4133
typestatestruct {
42-
rs*ResolvedSchema
34+
rs*Resolved
4335
depthint
4436
}
4537

@@ -60,10 +52,8 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
6052
returnfmt.Errorf("max recursion depth of %d reached",st.depth)
6153
}
6254

63-
// Treat the nil schema like the empty schema, as accepting everything.
64-
ifschema==nil {
65-
returnnil
66-
}
55+
// We checked for nil schemas in [Schema.Resolve].
56+
assert(schema!=nil,"nil schema")
6757

6858
// Step through interfaces.
6959
ifinstance.IsValid()&&instance.Kind()==reflect.Interface {
@@ -156,15 +146,8 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
156146
}
157147
}
158148

159-
ifschema.Pattern!="" {
160-
// TODO(jba): compile regexps during schema validation.
161-
m,err:=regexp.MatchString(schema.Pattern,str)
162-
iferr!=nil {
163-
returnerr
164-
}
165-
if!m {
166-
returnfmt.Errorf("pattern: %q does not match pattern %q",str,schema.Pattern)
167-
}
149+
ifschema.Pattern!=""&&!schema.pattern.MatchString(str) {
150+
returnfmt.Errorf("pattern: %q does not match regular expression %q",str,schema.Pattern)
168151
}
169152
}
170153

@@ -364,13 +347,8 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
364347
forvprop,val:=rangeinstance.Seq2() {
365348
prop:=vprop.String()
366349
// Check every matching pattern.
367-
forpattern,schema:=rangeschema.PatternProperties {
368-
// TODO(jba): pre-compile regexps
369-
m,err:=regexp.MatchString(pattern,prop)
370-
iferr!=nil {
371-
returnerr
372-
}
373-
ifm {
350+
forre,schema:=rangeschema.patternProperties {
351+
ifre.MatchString(prop) {
374352
iferr:=st.validate(val,schema,nil,append(path,prop));err!=nil {
375353
returnerr
376354
}

‎internal/mcp/internal/jsonschema/validate_test.go‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ func TestValidate(t *testing.T) {
5151
}
5252
for_,g:=rangegroups {
5353
t.Run(g.Description,func(t*testing.T) {
54-
rs:=&ResolvedSchema{root:g.Schema}
54+
rs,err:=g.Schema.Resolve()
55+
iferr!=nil {
56+
t.Fatal(err)
57+
}
5558
fors:=rangeg.Schema.all() {
5659
ifs.Defs!=nil||s.Ref!="" {
5760
t.Skip("schema or subschema has unimplemented keywords")

‎internal/mcp/mcp_test.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"time"
1616

1717
"github.com/google/go-cmp/cmp"
18+
"github.com/google/go-cmp/cmp/cmpopts"
1819
"golang.org/x/tools/internal/mcp/internal/jsonschema"
1920
"golang.org/x/tools/internal/mcp/internal/protocol"
2021
)
@@ -149,7 +150,7 @@ func TestEndToEnd(t *testing.T) {
149150
AdditionalProperties:falseSchema,
150151
},
151152
}}
152-
ifdiff:=cmp.Diff(wantTools,gotTools);diff!="" {
153+
ifdiff:=cmp.Diff(wantTools,gotTools,cmpopts.IgnoreUnexported(jsonschema.Schema{}));diff!="" {
153154
t.Fatalf("tools/list mismatch (-want +got):\n%s",diff)
154155
}
155156

‎internal/mcp/tool_test.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"testing"
1010

1111
"github.com/google/go-cmp/cmp"
12+
"github.com/google/go-cmp/cmp/cmpopts"
1213
"golang.org/x/tools/internal/mcp"
1314
"golang.org/x/tools/internal/mcp/internal/jsonschema"
1415
)
@@ -82,7 +83,7 @@ func TestMakeTool(t *testing.T) {
8283
},
8384
}
8485
for_,test:=rangetests {
85-
ifdiff:=cmp.Diff(test.want,test.tool.Definition.InputSchema);diff!="" {
86+
ifdiff:=cmp.Diff(test.want,test.tool.Definition.InputSchema,cmpopts.IgnoreUnexported(jsonschema.Schema{}));diff!="" {
8687
t.Errorf("MakeTool(%v) mismatch (-want +got):\n%s",test.tool.Definition.Name,diff)
8788
}
8889
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp