mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/yunionio/cloudpods.git
synced 2026-09-20 08:03:53 +08:00
fix(mcp-server): upgrade mcp lib (#25574)
This commit is contained in:
12
vendor/github.com/buger/jsonparser/.gitignore
generated
vendored
12
vendor/github.com/buger/jsonparser/.gitignore
generated
vendored
@@ -1,12 +0,0 @@
|
||||
|
||||
*.test
|
||||
|
||||
*.out
|
||||
|
||||
*.mprof
|
||||
|
||||
.idea
|
||||
|
||||
vendor/github.com/buger/goterm/
|
||||
prof.cpu
|
||||
prof.mem
|
||||
11
vendor/github.com/buger/jsonparser/.travis.yml
generated
vendored
11
vendor/github.com/buger/jsonparser/.travis.yml
generated
vendored
@@ -1,11 +0,0 @@
|
||||
language: go
|
||||
arch:
|
||||
- amd64
|
||||
- ppc64le
|
||||
go:
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
script: go test -v ./.
|
||||
12
vendor/github.com/buger/jsonparser/Dockerfile
generated
vendored
12
vendor/github.com/buger/jsonparser/Dockerfile
generated
vendored
@@ -1,12 +0,0 @@
|
||||
FROM golang:1.6
|
||||
|
||||
RUN go get github.com/Jeffail/gabs
|
||||
RUN go get github.com/bitly/go-simplejson
|
||||
RUN go get github.com/pquerna/ffjson
|
||||
RUN go get github.com/antonholmquist/jason
|
||||
RUN go get github.com/mreiferson/go-ujson
|
||||
RUN go get -tags=unsafe -u github.com/ugorji/go/codec
|
||||
RUN go get github.com/mailru/easyjson
|
||||
|
||||
WORKDIR /go/src/github.com/buger/jsonparser
|
||||
ADD . /go/src/github.com/buger/jsonparser
|
||||
36
vendor/github.com/buger/jsonparser/Makefile
generated
vendored
36
vendor/github.com/buger/jsonparser/Makefile
generated
vendored
@@ -1,36 +0,0 @@
|
||||
SOURCE = parser.go
|
||||
CONTAINER = jsonparser
|
||||
SOURCE_PATH = /go/src/github.com/buger/jsonparser
|
||||
BENCHMARK = JsonParser
|
||||
BENCHTIME = 5s
|
||||
TEST = .
|
||||
DRUN = docker run -v `pwd`:$(SOURCE_PATH) -i -t $(CONTAINER)
|
||||
|
||||
build:
|
||||
docker build -t $(CONTAINER) .
|
||||
|
||||
race:
|
||||
$(DRUN) --env GORACE="halt_on_error=1" go test ./. $(ARGS) -v -race -timeout 15s
|
||||
|
||||
bench:
|
||||
$(DRUN) go test $(LDFLAGS) -test.benchmem -bench $(BENCHMARK) ./benchmark/ $(ARGS) -benchtime $(BENCHTIME) -v
|
||||
|
||||
bench_local:
|
||||
$(DRUN) go test $(LDFLAGS) -test.benchmem -bench . $(ARGS) -benchtime $(BENCHTIME) -v
|
||||
|
||||
profile:
|
||||
$(DRUN) go test $(LDFLAGS) -test.benchmem -bench $(BENCHMARK) ./benchmark/ $(ARGS) -memprofile mem.mprof -v
|
||||
$(DRUN) go test $(LDFLAGS) -test.benchmem -bench $(BENCHMARK) ./benchmark/ $(ARGS) -cpuprofile cpu.out -v
|
||||
$(DRUN) go test $(LDFLAGS) -test.benchmem -bench $(BENCHMARK) ./benchmark/ $(ARGS) -c
|
||||
|
||||
test:
|
||||
$(DRUN) go test $(LDFLAGS) ./ -run $(TEST) -timeout 10s $(ARGS) -v
|
||||
|
||||
fmt:
|
||||
$(DRUN) go fmt ./...
|
||||
|
||||
vet:
|
||||
$(DRUN) go vet ./.
|
||||
|
||||
bash:
|
||||
$(DRUN) /bin/bash
|
||||
365
vendor/github.com/buger/jsonparser/README.md
generated
vendored
365
vendor/github.com/buger/jsonparser/README.md
generated
vendored
@@ -1,365 +0,0 @@
|
||||
[](https://goreportcard.com/report/github.com/buger/jsonparser) 
|
||||
# Alternative JSON parser for Go (10x times faster standard library)
|
||||
|
||||
It does not require you to know the structure of the payload (eg. create structs), and allows accessing fields by providing the path to them. It is up to **10 times faster** than standard `encoding/json` package (depending on payload size and usage), **allocates no memory**. See benchmarks below.
|
||||
|
||||
## Rationale
|
||||
Originally I made this for a project that relies on a lot of 3rd party APIs that can be unpredictable and complex.
|
||||
I love simplicity and prefer to avoid external dependecies. `encoding/json` requires you to know exactly your data structures, or if you prefer to use `map[string]interface{}` instead, it will be very slow and hard to manage.
|
||||
I investigated what's on the market and found that most libraries are just wrappers around `encoding/json`, there is few options with own parsers (`ffjson`, `easyjson`), but they still requires you to create data structures.
|
||||
|
||||
|
||||
Goal of this project is to push JSON parser to the performance limits and not sacrifice with compliance and developer user experience.
|
||||
|
||||
## Example
|
||||
For the given JSON our goal is to extract the user's full name, number of github followers and avatar.
|
||||
|
||||
```go
|
||||
import "github.com/buger/jsonparser"
|
||||
|
||||
...
|
||||
|
||||
data := []byte(`{
|
||||
"person": {
|
||||
"name": {
|
||||
"first": "Leonid",
|
||||
"last": "Bugaev",
|
||||
"fullName": "Leonid Bugaev"
|
||||
},
|
||||
"github": {
|
||||
"handle": "buger",
|
||||
"followers": 109
|
||||
},
|
||||
"avatars": [
|
||||
{ "url": "https://avatars1.githubusercontent.com/u/14009?v=3&s=460", "type": "thumbnail" }
|
||||
]
|
||||
},
|
||||
"company": {
|
||||
"name": "Acme"
|
||||
}
|
||||
}`)
|
||||
|
||||
// You can specify key path by providing arguments to Get function
|
||||
jsonparser.Get(data, "person", "name", "fullName")
|
||||
|
||||
// There is `GetInt` and `GetBoolean` helpers if you exactly know key data type
|
||||
jsonparser.GetInt(data, "person", "github", "followers")
|
||||
|
||||
// When you try to get object, it will return you []byte slice pointer to data containing it
|
||||
// In `company` it will be `{"name": "Acme"}`
|
||||
jsonparser.Get(data, "company")
|
||||
|
||||
// If the key doesn't exist it will throw an error
|
||||
var size int64
|
||||
if value, err := jsonparser.GetInt(data, "company", "size"); err == nil {
|
||||
size = value
|
||||
}
|
||||
|
||||
// You can use `ArrayEach` helper to iterate items [item1, item2 .... itemN]
|
||||
jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
|
||||
fmt.Println(jsonparser.Get(value, "url"))
|
||||
}, "person", "avatars")
|
||||
|
||||
// Or use can access fields by index!
|
||||
jsonparser.GetString(data, "person", "avatars", "[0]", "url")
|
||||
|
||||
// You can use `ObjectEach` helper to iterate objects { "key1":object1, "key2":object2, .... "keyN":objectN }
|
||||
jsonparser.ObjectEach(data, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error {
|
||||
fmt.Printf("Key: '%s'\n Value: '%s'\n Type: %s\n", string(key), string(value), dataType)
|
||||
return nil
|
||||
}, "person", "name")
|
||||
|
||||
// The most efficient way to extract multiple keys is `EachKey`
|
||||
|
||||
paths := [][]string{
|
||||
[]string{"person", "name", "fullName"},
|
||||
[]string{"person", "avatars", "[0]", "url"},
|
||||
[]string{"company", "url"},
|
||||
}
|
||||
jsonparser.EachKey(data, func(idx int, value []byte, vt jsonparser.ValueType, err error){
|
||||
switch idx {
|
||||
case 0: // []string{"person", "name", "fullName"}
|
||||
...
|
||||
case 1: // []string{"person", "avatars", "[0]", "url"}
|
||||
...
|
||||
case 2: // []string{"company", "url"},
|
||||
...
|
||||
}
|
||||
}, paths...)
|
||||
|
||||
// For more information see docs below
|
||||
```
|
||||
|
||||
## Need to speedup your app?
|
||||
|
||||
I'm available for consulting and can help you push your app performance to the limits. Ping me at: leonsbox@gmail.com.
|
||||
|
||||
## Reference
|
||||
|
||||
Library API is really simple. You just need the `Get` method to perform any operation. The rest is just helpers around it.
|
||||
|
||||
You also can view API at [godoc.org](https://godoc.org/github.com/buger/jsonparser)
|
||||
|
||||
|
||||
### **`Get`**
|
||||
```go
|
||||
func Get(data []byte, keys ...string) (value []byte, dataType jsonparser.ValueType, offset int, err error)
|
||||
```
|
||||
Receives data structure, and key path to extract value from.
|
||||
|
||||
Returns:
|
||||
* `value` - Pointer to original data structure containing key value, or just empty slice if nothing found or error
|
||||
* `dataType` - Can be: `NotExist`, `String`, `Number`, `Object`, `Array`, `Boolean` or `Null`
|
||||
* `offset` - Offset from provided data structure where key value ends. Used mostly internally, for example for `ArrayEach` helper.
|
||||
* `err` - If the key is not found or any other parsing issue, it should return error. If key not found it also sets `dataType` to `NotExist`
|
||||
|
||||
Accepts multiple keys to specify path to JSON value (in case of quering nested structures).
|
||||
If no keys are provided it will try to extract the closest JSON value (simple ones or object/array), useful for reading streams or arrays, see `ArrayEach` implementation.
|
||||
|
||||
Note that keys can be an array indexes: `jsonparser.GetInt("person", "avatars", "[0]", "url")`, pretty cool, yeah?
|
||||
|
||||
### **`GetString`**
|
||||
```go
|
||||
func GetString(data []byte, keys ...string) (val string, err error)
|
||||
```
|
||||
Returns strings properly handing escaped and unicode characters. Note that this will cause additional memory allocations.
|
||||
|
||||
### **`GetUnsafeString`**
|
||||
If you need string in your app, and ready to sacrifice with support of escaped symbols in favor of speed. It returns string mapped to existing byte slice memory, without any allocations:
|
||||
```go
|
||||
s, _, := jsonparser.GetUnsafeString(data, "person", "name", "title")
|
||||
switch s {
|
||||
case 'CEO':
|
||||
...
|
||||
case 'Engineer'
|
||||
...
|
||||
...
|
||||
}
|
||||
```
|
||||
Note that `unsafe` here means that your string will exist until GC will free underlying byte slice, for most of cases it means that you can use this string only in current context, and should not pass it anywhere externally: through channels or any other way.
|
||||
|
||||
|
||||
### **`GetBoolean`**, **`GetInt`** and **`GetFloat`**
|
||||
```go
|
||||
func GetBoolean(data []byte, keys ...string) (val bool, err error)
|
||||
|
||||
func GetFloat(data []byte, keys ...string) (val float64, err error)
|
||||
|
||||
func GetInt(data []byte, keys ...string) (val int64, err error)
|
||||
```
|
||||
If you know the key type, you can use the helpers above.
|
||||
If key data type do not match, it will return error.
|
||||
|
||||
### **`ArrayEach`**
|
||||
```go
|
||||
func ArrayEach(data []byte, cb func(value []byte, dataType jsonparser.ValueType, offset int, err error), keys ...string)
|
||||
```
|
||||
Needed for iterating arrays, accepts a callback function with the same return arguments as `Get`.
|
||||
|
||||
### **`ObjectEach`**
|
||||
```go
|
||||
func ObjectEach(data []byte, callback func(key []byte, value []byte, dataType ValueType, offset int) error, keys ...string) (err error)
|
||||
```
|
||||
Needed for iterating object, accepts a callback function. Example:
|
||||
```go
|
||||
var handler func([]byte, []byte, jsonparser.ValueType, int) error
|
||||
handler = func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error {
|
||||
//do stuff here
|
||||
}
|
||||
jsonparser.ObjectEach(myJson, handler)
|
||||
```
|
||||
|
||||
|
||||
### **`EachKey`**
|
||||
```go
|
||||
func EachKey(data []byte, cb func(idx int, value []byte, dataType jsonparser.ValueType, err error), paths ...[]string)
|
||||
```
|
||||
When you need to read multiple keys, and you do not afraid of low-level API `EachKey` is your friend. It read payload only single time, and calls callback function once path is found. For example when you call multiple times `Get`, it has to process payload multiple times, each time you call it. Depending on payload `EachKey` can be multiple times faster than `Get`. Path can use nested keys as well!
|
||||
|
||||
```go
|
||||
paths := [][]string{
|
||||
[]string{"uuid"},
|
||||
[]string{"tz"},
|
||||
[]string{"ua"},
|
||||
[]string{"st"},
|
||||
}
|
||||
var data SmallPayload
|
||||
|
||||
jsonparser.EachKey(smallFixture, func(idx int, value []byte, vt jsonparser.ValueType, err error){
|
||||
switch idx {
|
||||
case 0:
|
||||
data.Uuid, _ = value
|
||||
case 1:
|
||||
v, _ := jsonparser.ParseInt(value)
|
||||
data.Tz = int(v)
|
||||
case 2:
|
||||
data.Ua, _ = value
|
||||
case 3:
|
||||
v, _ := jsonparser.ParseInt(value)
|
||||
data.St = int(v)
|
||||
}
|
||||
}, paths...)
|
||||
```
|
||||
|
||||
### **`Set`**
|
||||
```go
|
||||
func Set(data []byte, setValue []byte, keys ...string) (value []byte, err error)
|
||||
```
|
||||
Receives existing data structure, key path to set, and value to set at that key. *This functionality is experimental.*
|
||||
|
||||
Returns:
|
||||
* `value` - Pointer to original data structure with updated or added key value.
|
||||
* `err` - If any parsing issue, it should return error.
|
||||
|
||||
Accepts multiple keys to specify path to JSON value (in case of updating or creating nested structures).
|
||||
|
||||
Note that keys can be an array indexes: `jsonparser.Set(data, []byte("http://github.com"), "person", "avatars", "[0]", "url")`
|
||||
|
||||
### **`Delete`**
|
||||
```go
|
||||
func Delete(data []byte, keys ...string) value []byte
|
||||
```
|
||||
Receives existing data structure, and key path to delete. *This functionality is experimental.*
|
||||
|
||||
Returns:
|
||||
* `value` - Pointer to original data structure with key path deleted if it can be found. If there is no key path, then the whole data structure is deleted.
|
||||
|
||||
Accepts multiple keys to specify path to JSON value (in case of updating or creating nested structures).
|
||||
|
||||
Note that keys can be an array indexes: `jsonparser.Delete(data, "person", "avatars", "[0]", "url")`
|
||||
|
||||
|
||||
## What makes it so fast?
|
||||
* It does not rely on `encoding/json`, `reflection` or `interface{}`, the only real package dependency is `bytes`.
|
||||
* Operates with JSON payload on byte level, providing you pointers to the original data structure: no memory allocation.
|
||||
* No automatic type conversions, by default everything is a []byte, but it provides you value type, so you can convert by yourself (there is few helpers included).
|
||||
* Does not parse full record, only keys you specified
|
||||
|
||||
|
||||
## Benchmarks
|
||||
|
||||
There are 3 benchmark types, trying to simulate real-life usage for small, medium and large JSON payloads.
|
||||
For each metric, the lower value is better. Time/op is in nanoseconds. Values better than standard encoding/json marked as bold text.
|
||||
Benchmarks run on standard Linode 1024 box.
|
||||
|
||||
Compared libraries:
|
||||
* https://golang.org/pkg/encoding/json
|
||||
* https://github.com/Jeffail/gabs
|
||||
* https://github.com/a8m/djson
|
||||
* https://github.com/bitly/go-simplejson
|
||||
* https://github.com/antonholmquist/jason
|
||||
* https://github.com/mreiferson/go-ujson
|
||||
* https://github.com/ugorji/go/codec
|
||||
* https://github.com/pquerna/ffjson
|
||||
* https://github.com/mailru/easyjson
|
||||
* https://github.com/buger/jsonparser
|
||||
|
||||
#### TLDR
|
||||
If you want to skip next sections we have 2 winner: `jsonparser` and `easyjson`.
|
||||
`jsonparser` is up to 10 times faster than standard `encoding/json` package (depending on payload size and usage), and almost infinitely (literally) better in memory consumption because it operates with data on byte level, and provide direct slice pointers.
|
||||
`easyjson` wins in CPU in medium tests and frankly i'm impressed with this package: it is remarkable results considering that it is almost drop-in replacement for `encoding/json` (require some code generation).
|
||||
|
||||
It's hard to fully compare `jsonparser` and `easyjson` (or `ffson`), they a true parsers and fully process record, unlike `jsonparser` which parse only keys you specified.
|
||||
|
||||
If you searching for replacement of `encoding/json` while keeping structs, `easyjson` is an amazing choice. If you want to process dynamic JSON, have memory constrains, or more control over your data you should try `jsonparser`.
|
||||
|
||||
`jsonparser` performance heavily depends on usage, and it works best when you do not need to process full record, only some keys. The more calls you need to make, the slower it will be, in contrast `easyjson` (or `ffjson`, `encoding/json`) parser record only 1 time, and then you can make as many calls as you want.
|
||||
|
||||
With great power comes great responsibility! :)
|
||||
|
||||
|
||||
#### Small payload
|
||||
|
||||
Each test processes 190 bytes of http log as a JSON record.
|
||||
It should read multiple fields.
|
||||
https://github.com/buger/jsonparser/blob/master/benchmark/benchmark_small_payload_test.go
|
||||
|
||||
Library | time/op | bytes/op | allocs/op
|
||||
------ | ------- | -------- | -------
|
||||
encoding/json struct | 7879 | 880 | 18
|
||||
encoding/json interface{} | 8946 | 1521 | 38
|
||||
Jeffail/gabs | 10053 | 1649 | 46
|
||||
bitly/go-simplejson | 10128 | 2241 | 36
|
||||
antonholmquist/jason | 27152 | 7237 | 101
|
||||
github.com/ugorji/go/codec | 8806 | 2176 | 31
|
||||
mreiferson/go-ujson | **7008** | **1409** | 37
|
||||
a8m/djson | 3862 | 1249 | 30
|
||||
pquerna/ffjson | **3769** | **624** | **15**
|
||||
mailru/easyjson | **2002** | **192** | **9**
|
||||
buger/jsonparser | **1367** | **0** | **0**
|
||||
buger/jsonparser (EachKey API) | **809** | **0** | **0**
|
||||
|
||||
Winners are ffjson, easyjson and jsonparser, where jsonparser is up to 9.8x faster than encoding/json and 4.6x faster than ffjson, and slightly faster than easyjson.
|
||||
If you look at memory allocation, jsonparser has no rivals, as it makes no data copy and operates with raw []byte structures and pointers to it.
|
||||
|
||||
#### Medium payload
|
||||
|
||||
Each test processes a 2.4kb JSON record (based on Clearbit API).
|
||||
It should read multiple nested fields and 1 array.
|
||||
|
||||
https://github.com/buger/jsonparser/blob/master/benchmark/benchmark_medium_payload_test.go
|
||||
|
||||
| Library | time/op | bytes/op | allocs/op |
|
||||
| ------- | ------- | -------- | --------- |
|
||||
| encoding/json struct | 57749 | 1336 | 29 |
|
||||
| encoding/json interface{} | 79297 | 10627 | 215 |
|
||||
| Jeffail/gabs | 83807 | 11202 | 235 |
|
||||
| bitly/go-simplejson | 88187 | 17187 | 220 |
|
||||
| antonholmquist/jason | 94099 | 19013 | 247 |
|
||||
| github.com/ugorji/go/codec | 114719 | 6712 | 152 |
|
||||
| mreiferson/go-ujson | **56972** | 11547 | 270 |
|
||||
| a8m/djson | 28525 | 10196 | 198 |
|
||||
| pquerna/ffjson | **20298** | **856** | **20** |
|
||||
| mailru/easyjson | **10512** | **336** | **12** |
|
||||
| buger/jsonparser | **15955** | **0** | **0** |
|
||||
| buger/jsonparser (EachKey API) | **8916** | **0** | **0** |
|
||||
|
||||
The difference between ffjson and jsonparser in CPU usage is smaller, while the memory consumption difference is growing. On the other hand `easyjson` shows remarkable performance for medium payload.
|
||||
|
||||
`gabs`, `go-simplejson` and `jason` are based on encoding/json and map[string]interface{} and actually only helpers for unstructured JSON, their performance correlate with `encoding/json interface{}`, and they will skip next round.
|
||||
`go-ujson` while have its own parser, shows same performance as `encoding/json`, also skips next round. Same situation with `ugorji/go/codec`, but it showed unexpectedly bad performance for complex payloads.
|
||||
|
||||
|
||||
#### Large payload
|
||||
|
||||
Each test processes a 24kb JSON record (based on Discourse API)
|
||||
It should read 2 arrays, and for each item in array get a few fields.
|
||||
Basically it means processing a full JSON file.
|
||||
|
||||
https://github.com/buger/jsonparser/blob/master/benchmark/benchmark_large_payload_test.go
|
||||
|
||||
| Library | time/op | bytes/op | allocs/op |
|
||||
| --- | --- | --- | --- |
|
||||
| encoding/json struct | 748336 | 8272 | 307 |
|
||||
| encoding/json interface{} | 1224271 | 215425 | 3395 |
|
||||
| a8m/djson | 510082 | 213682 | 2845 |
|
||||
| pquerna/ffjson | **312271** | **7792** | **298** |
|
||||
| mailru/easyjson | **154186** | **6992** | **288** |
|
||||
| buger/jsonparser | **85308** | **0** | **0** |
|
||||
|
||||
`jsonparser` now is a winner, but do not forget that it is way more lightweight parser than `ffson` or `easyjson`, and they have to parser all the data, while `jsonparser` parse only what you need. All `ffjson`, `easysjon` and `jsonparser` have their own parsing code, and does not depend on `encoding/json` or `interface{}`, thats one of the reasons why they are so fast. `easyjson` also use a bit of `unsafe` package to reduce memory consuption (in theory it can lead to some unexpected GC issue, but i did not tested enough)
|
||||
|
||||
Also last benchmark did not included `EachKey` test, because in this particular case we need to read lot of Array values, and using `ArrayEach` is more efficient.
|
||||
|
||||
## Questions and support
|
||||
|
||||
All bug-reports and suggestions should go though Github Issues.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork it
|
||||
2. Create your feature branch (git checkout -b my-new-feature)
|
||||
3. Commit your changes (git commit -am 'Added some feature')
|
||||
4. Push to the branch (git push origin my-new-feature)
|
||||
5. Create new Pull Request
|
||||
|
||||
## Development
|
||||
|
||||
All my development happens using Docker, and repo include some Make tasks to simplify development.
|
||||
|
||||
* `make build` - builds docker image, usually can be called only once
|
||||
* `make test` - run tests
|
||||
* `make fmt` - run go fmt
|
||||
* `make bench` - run benchmarks (if you need to run only single benchmark modify `BENCHMARK` variable in make file)
|
||||
* `make profile` - runs benchmark and generate 3 files- `cpu.out`, `mem.mprof` and `benchmark.test` binary, which can be used for `go tool pprof`
|
||||
* `make bash` - enter container (i use it for running `go tool pprof` above)
|
||||
47
vendor/github.com/buger/jsonparser/bytes.go
generated
vendored
47
vendor/github.com/buger/jsonparser/bytes.go
generated
vendored
@@ -1,47 +0,0 @@
|
||||
package jsonparser
|
||||
|
||||
import (
|
||||
bio "bytes"
|
||||
)
|
||||
|
||||
// minInt64 '-9223372036854775808' is the smallest representable number in int64
|
||||
const minInt64 = `9223372036854775808`
|
||||
|
||||
// About 2x faster then strconv.ParseInt because it only supports base 10, which is enough for JSON
|
||||
func parseInt(bytes []byte) (v int64, ok bool, overflow bool) {
|
||||
if len(bytes) == 0 {
|
||||
return 0, false, false
|
||||
}
|
||||
|
||||
var neg bool = false
|
||||
if bytes[0] == '-' {
|
||||
neg = true
|
||||
bytes = bytes[1:]
|
||||
}
|
||||
|
||||
var b int64 = 0
|
||||
for _, c := range bytes {
|
||||
if c >= '0' && c <= '9' {
|
||||
b = (10 * v) + int64(c-'0')
|
||||
} else {
|
||||
return 0, false, false
|
||||
}
|
||||
if overflow = (b < v); overflow {
|
||||
break
|
||||
}
|
||||
v = b
|
||||
}
|
||||
|
||||
if overflow {
|
||||
if neg && bio.Equal(bytes, []byte(minInt64)) {
|
||||
return b, true, false
|
||||
}
|
||||
return 0, false, true
|
||||
}
|
||||
|
||||
if neg {
|
||||
return -v, true, false
|
||||
} else {
|
||||
return v, true, false
|
||||
}
|
||||
}
|
||||
25
vendor/github.com/buger/jsonparser/bytes_safe.go
generated
vendored
25
vendor/github.com/buger/jsonparser/bytes_safe.go
generated
vendored
@@ -1,25 +0,0 @@
|
||||
// +build appengine appenginevm
|
||||
|
||||
package jsonparser
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// See fastbytes_unsafe.go for explanation on why *[]byte is used (signatures must be consistent with those in that file)
|
||||
|
||||
func equalStr(b *[]byte, s string) bool {
|
||||
return string(*b) == s
|
||||
}
|
||||
|
||||
func parseFloat(b *[]byte) (float64, error) {
|
||||
return strconv.ParseFloat(string(*b), 64)
|
||||
}
|
||||
|
||||
func bytesToString(b *[]byte) string {
|
||||
return string(*b)
|
||||
}
|
||||
|
||||
func StringToBytes(s string) []byte {
|
||||
return []byte(s)
|
||||
}
|
||||
44
vendor/github.com/buger/jsonparser/bytes_unsafe.go
generated
vendored
44
vendor/github.com/buger/jsonparser/bytes_unsafe.go
generated
vendored
@@ -1,44 +0,0 @@
|
||||
// +build !appengine,!appenginevm
|
||||
|
||||
package jsonparser
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"unsafe"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
//
|
||||
// The reason for using *[]byte rather than []byte in parameters is an optimization. As of Go 1.6,
|
||||
// the compiler cannot perfectly inline the function when using a non-pointer slice. That is,
|
||||
// the non-pointer []byte parameter version is slower than if its function body is manually
|
||||
// inlined, whereas the pointer []byte version is equally fast to the manually inlined
|
||||
// version. Instruction count in assembly taken from "go tool compile" confirms this difference.
|
||||
//
|
||||
// TODO: Remove hack after Go 1.7 release
|
||||
//
|
||||
func equalStr(b *[]byte, s string) bool {
|
||||
return *(*string)(unsafe.Pointer(b)) == s
|
||||
}
|
||||
|
||||
func parseFloat(b *[]byte) (float64, error) {
|
||||
return strconv.ParseFloat(*(*string)(unsafe.Pointer(b)), 64)
|
||||
}
|
||||
|
||||
// A hack until issue golang/go#2632 is fixed.
|
||||
// See: https://github.com/golang/go/issues/2632
|
||||
func bytesToString(b *[]byte) string {
|
||||
return *(*string)(unsafe.Pointer(b))
|
||||
}
|
||||
|
||||
func StringToBytes(s string) []byte {
|
||||
b := make([]byte, 0, 0)
|
||||
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
|
||||
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
|
||||
bh.Data = sh.Data
|
||||
bh.Cap = sh.Len
|
||||
bh.Len = sh.Len
|
||||
runtime.KeepAlive(s)
|
||||
return b
|
||||
}
|
||||
173
vendor/github.com/buger/jsonparser/escape.go
generated
vendored
173
vendor/github.com/buger/jsonparser/escape.go
generated
vendored
@@ -1,173 +0,0 @@
|
||||
package jsonparser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// JSON Unicode stuff: see https://tools.ietf.org/html/rfc7159#section-7
|
||||
|
||||
const supplementalPlanesOffset = 0x10000
|
||||
const highSurrogateOffset = 0xD800
|
||||
const lowSurrogateOffset = 0xDC00
|
||||
|
||||
const basicMultilingualPlaneReservedOffset = 0xDFFF
|
||||
const basicMultilingualPlaneOffset = 0xFFFF
|
||||
|
||||
func combineUTF16Surrogates(high, low rune) rune {
|
||||
return supplementalPlanesOffset + (high-highSurrogateOffset)<<10 + (low - lowSurrogateOffset)
|
||||
}
|
||||
|
||||
const badHex = -1
|
||||
|
||||
func h2I(c byte) int {
|
||||
switch {
|
||||
case c >= '0' && c <= '9':
|
||||
return int(c - '0')
|
||||
case c >= 'A' && c <= 'F':
|
||||
return int(c - 'A' + 10)
|
||||
case c >= 'a' && c <= 'f':
|
||||
return int(c - 'a' + 10)
|
||||
}
|
||||
return badHex
|
||||
}
|
||||
|
||||
// decodeSingleUnicodeEscape decodes a single \uXXXX escape sequence. The prefix \u is assumed to be present and
|
||||
// is not checked.
|
||||
// In JSON, these escapes can either come alone or as part of "UTF16 surrogate pairs" that must be handled together.
|
||||
// This function only handles one; decodeUnicodeEscape handles this more complex case.
|
||||
func decodeSingleUnicodeEscape(in []byte) (rune, bool) {
|
||||
// We need at least 6 characters total
|
||||
if len(in) < 6 {
|
||||
return utf8.RuneError, false
|
||||
}
|
||||
|
||||
// Convert hex to decimal
|
||||
h1, h2, h3, h4 := h2I(in[2]), h2I(in[3]), h2I(in[4]), h2I(in[5])
|
||||
if h1 == badHex || h2 == badHex || h3 == badHex || h4 == badHex {
|
||||
return utf8.RuneError, false
|
||||
}
|
||||
|
||||
// Compose the hex digits
|
||||
return rune(h1<<12 + h2<<8 + h3<<4 + h4), true
|
||||
}
|
||||
|
||||
// isUTF16EncodedRune checks if a rune is in the range for non-BMP characters,
|
||||
// which is used to describe UTF16 chars.
|
||||
// Source: https://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane
|
||||
func isUTF16EncodedRune(r rune) bool {
|
||||
return highSurrogateOffset <= r && r <= basicMultilingualPlaneReservedOffset
|
||||
}
|
||||
|
||||
func decodeUnicodeEscape(in []byte) (rune, int) {
|
||||
if r, ok := decodeSingleUnicodeEscape(in); !ok {
|
||||
// Invalid Unicode escape
|
||||
return utf8.RuneError, -1
|
||||
} else if r <= basicMultilingualPlaneOffset && !isUTF16EncodedRune(r) {
|
||||
// Valid Unicode escape in Basic Multilingual Plane
|
||||
return r, 6
|
||||
} else if r2, ok := decodeSingleUnicodeEscape(in[6:]); !ok { // Note: previous decodeSingleUnicodeEscape success guarantees at least 6 bytes remain
|
||||
// UTF16 "high surrogate" without manditory valid following Unicode escape for the "low surrogate"
|
||||
return utf8.RuneError, -1
|
||||
} else if r2 < lowSurrogateOffset {
|
||||
// Invalid UTF16 "low surrogate"
|
||||
return utf8.RuneError, -1
|
||||
} else {
|
||||
// Valid UTF16 surrogate pair
|
||||
return combineUTF16Surrogates(r, r2), 12
|
||||
}
|
||||
}
|
||||
|
||||
// backslashCharEscapeTable: when '\X' is found for some byte X, it is to be replaced with backslashCharEscapeTable[X]
|
||||
var backslashCharEscapeTable = [...]byte{
|
||||
'"': '"',
|
||||
'\\': '\\',
|
||||
'/': '/',
|
||||
'b': '\b',
|
||||
'f': '\f',
|
||||
'n': '\n',
|
||||
'r': '\r',
|
||||
't': '\t',
|
||||
}
|
||||
|
||||
// unescapeToUTF8 unescapes the single escape sequence starting at 'in' into 'out' and returns
|
||||
// how many characters were consumed from 'in' and emitted into 'out'.
|
||||
// If a valid escape sequence does not appear as a prefix of 'in', (-1, -1) to signal the error.
|
||||
func unescapeToUTF8(in, out []byte) (inLen int, outLen int) {
|
||||
if len(in) < 2 || in[0] != '\\' {
|
||||
// Invalid escape due to insufficient characters for any escape or no initial backslash
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc7159#section-7
|
||||
switch e := in[1]; e {
|
||||
case '"', '\\', '/', 'b', 'f', 'n', 'r', 't':
|
||||
// Valid basic 2-character escapes (use lookup table)
|
||||
out[0] = backslashCharEscapeTable[e]
|
||||
return 2, 1
|
||||
case 'u':
|
||||
// Unicode escape
|
||||
if r, inLen := decodeUnicodeEscape(in); inLen == -1 {
|
||||
// Invalid Unicode escape
|
||||
return -1, -1
|
||||
} else {
|
||||
// Valid Unicode escape; re-encode as UTF8
|
||||
outLen := utf8.EncodeRune(out, r)
|
||||
return inLen, outLen
|
||||
}
|
||||
}
|
||||
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
// unescape unescapes the string contained in 'in' and returns it as a slice.
|
||||
// If 'in' contains no escaped characters:
|
||||
// Returns 'in'.
|
||||
// Else, if 'out' is of sufficient capacity (guaranteed if cap(out) >= len(in)):
|
||||
// 'out' is used to build the unescaped string and is returned with no extra allocation
|
||||
// Else:
|
||||
// A new slice is allocated and returned.
|
||||
func Unescape(in, out []byte) ([]byte, error) {
|
||||
firstBackslash := bytes.IndexByte(in, '\\')
|
||||
if firstBackslash == -1 {
|
||||
return in, nil
|
||||
}
|
||||
|
||||
// Get a buffer of sufficient size (allocate if needed)
|
||||
if cap(out) < len(in) {
|
||||
out = make([]byte, len(in))
|
||||
} else {
|
||||
out = out[0:len(in)]
|
||||
}
|
||||
|
||||
// Copy the first sequence of unescaped bytes to the output and obtain a buffer pointer (subslice)
|
||||
copy(out, in[:firstBackslash])
|
||||
in = in[firstBackslash:]
|
||||
buf := out[firstBackslash:]
|
||||
|
||||
for len(in) > 0 {
|
||||
// Unescape the next escaped character
|
||||
inLen, bufLen := unescapeToUTF8(in, buf)
|
||||
if inLen == -1 {
|
||||
return nil, MalformedStringEscapeError
|
||||
}
|
||||
|
||||
in = in[inLen:]
|
||||
buf = buf[bufLen:]
|
||||
|
||||
// Copy everything up until the next backslash
|
||||
nextBackslash := bytes.IndexByte(in, '\\')
|
||||
if nextBackslash == -1 {
|
||||
copy(buf, in)
|
||||
buf = buf[len(in):]
|
||||
break
|
||||
} else {
|
||||
copy(buf, in[:nextBackslash])
|
||||
buf = buf[nextBackslash:]
|
||||
in = in[nextBackslash:]
|
||||
}
|
||||
}
|
||||
|
||||
// Trim the out buffer to the amount that was actually emitted
|
||||
return out[:len(out)-len(buf)], nil
|
||||
}
|
||||
117
vendor/github.com/buger/jsonparser/fuzz.go
generated
vendored
117
vendor/github.com/buger/jsonparser/fuzz.go
generated
vendored
@@ -1,117 +0,0 @@
|
||||
package jsonparser
|
||||
|
||||
func FuzzParseString(data []byte) int {
|
||||
r, err := ParseString(data)
|
||||
if err != nil || r == "" {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func FuzzEachKey(data []byte) int {
|
||||
paths := [][]string{
|
||||
{"name"},
|
||||
{"order"},
|
||||
{"nested", "a"},
|
||||
{"nested", "b"},
|
||||
{"nested2", "a"},
|
||||
{"nested", "nested3", "b"},
|
||||
{"arr", "[1]", "b"},
|
||||
{"arrInt", "[3]"},
|
||||
{"arrInt", "[5]"},
|
||||
{"nested"},
|
||||
{"arr", "["},
|
||||
{"a\n", "b\n"},
|
||||
}
|
||||
EachKey(data, func(idx int, value []byte, vt ValueType, err error) {}, paths...)
|
||||
return 1
|
||||
}
|
||||
|
||||
func FuzzDelete(data []byte) int {
|
||||
Delete(data, "test")
|
||||
return 1
|
||||
}
|
||||
|
||||
func FuzzSet(data []byte) int {
|
||||
_, err := Set(data, []byte(`"new value"`), "test")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func FuzzObjectEach(data []byte) int {
|
||||
_ = ObjectEach(data, func(key, value []byte, valueType ValueType, off int) error {
|
||||
return nil
|
||||
})
|
||||
return 1
|
||||
}
|
||||
|
||||
func FuzzParseFloat(data []byte) int {
|
||||
_, err := ParseFloat(data)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func FuzzParseInt(data []byte) int {
|
||||
_, err := ParseInt(data)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func FuzzParseBool(data []byte) int {
|
||||
_, err := ParseBoolean(data)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func FuzzTokenStart(data []byte) int {
|
||||
_ = tokenStart(data)
|
||||
return 1
|
||||
}
|
||||
|
||||
func FuzzGetString(data []byte) int {
|
||||
_, err := GetString(data, "test")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func FuzzGetFloat(data []byte) int {
|
||||
_, err := GetFloat(data, "test")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func FuzzGetInt(data []byte) int {
|
||||
_, err := GetInt(data, "test")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func FuzzGetBoolean(data []byte) int {
|
||||
_, err := GetBoolean(data, "test")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func FuzzGetUnsafeString(data []byte) int {
|
||||
_, err := GetUnsafeString(data, "test")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
47
vendor/github.com/buger/jsonparser/oss-fuzz-build.sh
generated
vendored
47
vendor/github.com/buger/jsonparser/oss-fuzz-build.sh
generated
vendored
@@ -1,47 +0,0 @@
|
||||
#!/bin/bash -eu
|
||||
|
||||
git clone https://github.com/dvyukov/go-fuzz-corpus
|
||||
zip corpus.zip go-fuzz-corpus/json/corpus/*
|
||||
|
||||
cp corpus.zip $OUT/fuzzparsestring_seed_corpus.zip
|
||||
compile_go_fuzzer github.com/buger/jsonparser FuzzParseString fuzzparsestring
|
||||
|
||||
cp corpus.zip $OUT/fuzzeachkey_seed_corpus.zip
|
||||
compile_go_fuzzer github.com/buger/jsonparser FuzzEachKey fuzzeachkey
|
||||
|
||||
cp corpus.zip $OUT/fuzzdelete_seed_corpus.zip
|
||||
compile_go_fuzzer github.com/buger/jsonparser FuzzDelete fuzzdelete
|
||||
|
||||
cp corpus.zip $OUT/fuzzset_seed_corpus.zip
|
||||
compile_go_fuzzer github.com/buger/jsonparser FuzzSet fuzzset
|
||||
|
||||
cp corpus.zip $OUT/fuzzobjecteach_seed_corpus.zip
|
||||
compile_go_fuzzer github.com/buger/jsonparser FuzzObjectEach fuzzobjecteach
|
||||
|
||||
cp corpus.zip $OUT/fuzzparsefloat_seed_corpus.zip
|
||||
compile_go_fuzzer github.com/buger/jsonparser FuzzParseFloat fuzzparsefloat
|
||||
|
||||
cp corpus.zip $OUT/fuzzparseint_seed_corpus.zip
|
||||
compile_go_fuzzer github.com/buger/jsonparser FuzzParseInt fuzzparseint
|
||||
|
||||
cp corpus.zip $OUT/fuzzparsebool_seed_corpus.zip
|
||||
compile_go_fuzzer github.com/buger/jsonparser FuzzParseBool fuzzparsebool
|
||||
|
||||
cp corpus.zip $OUT/fuzztokenstart_seed_corpus.zip
|
||||
compile_go_fuzzer github.com/buger/jsonparser FuzzTokenStart fuzztokenstart
|
||||
|
||||
cp corpus.zip $OUT/fuzzgetstring_seed_corpus.zip
|
||||
compile_go_fuzzer github.com/buger/jsonparser FuzzGetString fuzzgetstring
|
||||
|
||||
cp corpus.zip $OUT/fuzzgetfloat_seed_corpus.zip
|
||||
compile_go_fuzzer github.com/buger/jsonparser FuzzGetFloat fuzzgetfloat
|
||||
|
||||
cp corpus.zip $OUT/fuzzgetint_seed_corpus.zip
|
||||
compile_go_fuzzer github.com/buger/jsonparser FuzzGetInt fuzzgetint
|
||||
|
||||
cp corpus.zip $OUT/fuzzgetboolean_seed_corpus.zip
|
||||
compile_go_fuzzer github.com/buger/jsonparser FuzzGetBoolean fuzzgetboolean
|
||||
|
||||
cp corpus.zip $OUT/fuzzgetunsafestring_seed_corpus.zip
|
||||
compile_go_fuzzer github.com/buger/jsonparser FuzzGetUnsafeString fuzzgetunsafestring
|
||||
|
||||
1283
vendor/github.com/buger/jsonparser/parser.go
generated
vendored
1283
vendor/github.com/buger/jsonparser/parser.go
generated
vendored
File diff suppressed because it is too large
Load Diff
7
vendor/github.com/google/go-cmp/cmp/internal/function/func.go
generated
vendored
7
vendor/github.com/google/go-cmp/cmp/internal/function/func.go
generated
vendored
@@ -19,6 +19,7 @@ const (
|
||||
|
||||
tbFunc // func(T) bool
|
||||
ttbFunc // func(T, T) bool
|
||||
ttiFunc // func(T, T) int
|
||||
trbFunc // func(T, R) bool
|
||||
tibFunc // func(T, I) bool
|
||||
trFunc // func(T) R
|
||||
@@ -28,11 +29,13 @@ const (
|
||||
Transformer = trFunc // func(T) R
|
||||
ValueFilter = ttbFunc // func(T, T) bool
|
||||
Less = ttbFunc // func(T, T) bool
|
||||
Compare = ttiFunc // func(T, T) int
|
||||
ValuePredicate = tbFunc // func(T) bool
|
||||
KeyValuePredicate = trbFunc // func(T, R) bool
|
||||
)
|
||||
|
||||
var boolType = reflect.TypeOf(true)
|
||||
var intType = reflect.TypeOf(0)
|
||||
|
||||
// IsType reports whether the reflect.Type is of the specified function type.
|
||||
func IsType(t reflect.Type, ft funcType) bool {
|
||||
@@ -49,6 +52,10 @@ func IsType(t reflect.Type, ft funcType) bool {
|
||||
if ni == 2 && no == 1 && t.In(0) == t.In(1) && t.Out(0) == boolType {
|
||||
return true
|
||||
}
|
||||
case ttiFunc: // func(T, T) int
|
||||
if ni == 2 && no == 1 && t.In(0) == t.In(1) && t.Out(0) == intType {
|
||||
return true
|
||||
}
|
||||
case trbFunc: // func(T, R) bool
|
||||
if ni == 2 && no == 1 && t.Out(0) == boolType {
|
||||
return true
|
||||
|
||||
10
vendor/github.com/google/go-cmp/cmp/options.go
generated
vendored
10
vendor/github.com/google/go-cmp/cmp/options.go
generated
vendored
@@ -232,7 +232,15 @@ func (validator) apply(s *state, vx, vy reflect.Value) {
|
||||
if t := s.curPath.Index(-2).Type(); t.Name() != "" {
|
||||
// Named type with unexported fields.
|
||||
name = fmt.Sprintf("%q.%v", t.PkgPath(), t.Name()) // e.g., "path/to/package".MyType
|
||||
if _, ok := reflect.New(t).Interface().(error); ok {
|
||||
isProtoMessage := func(t reflect.Type) bool {
|
||||
m, ok := reflect.PointerTo(t).MethodByName("ProtoReflect")
|
||||
return ok && m.Type.NumIn() == 1 && m.Type.NumOut() == 1 &&
|
||||
m.Type.Out(0).PkgPath() == "google.golang.org/protobuf/reflect/protoreflect" &&
|
||||
m.Type.Out(0).Name() == "Message"
|
||||
}
|
||||
if isProtoMessage(t) {
|
||||
help = `consider using "google.golang.org/protobuf/testing/protocmp".Transform to compare proto.Message types`
|
||||
} else if _, ok := reflect.New(t).Interface().(error); ok {
|
||||
help = "consider using cmpopts.EquateErrors to compare error values"
|
||||
} else if t.Comparable() {
|
||||
help = "consider using cmpopts.EquateComparable to compare comparable Go types"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Leonid Bugaev
|
||||
Copyright (c) 2025 JSON Schema Go Project Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
76
vendor/github.com/google/jsonschema-go/jsonschema/annotations.go
generated
vendored
Normal file
76
vendor/github.com/google/jsonschema-go/jsonschema/annotations.go
generated
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package jsonschema
|
||||
|
||||
import "maps"
|
||||
|
||||
// An annotations tracks certain properties computed by keywords that are used by validation.
|
||||
// ("Annotation" is the spec's term.)
|
||||
// In particular, the unevaluatedItems and unevaluatedProperties keywords need to know which
|
||||
// items and properties were evaluated (validated successfully).
|
||||
type annotations struct {
|
||||
allItems bool // all items were evaluated
|
||||
endIndex int // 1+largest index evaluated by prefixItems
|
||||
evaluatedIndexes map[int]bool // set of indexes evaluated by contains
|
||||
allProperties bool // all properties were evaluated
|
||||
evaluatedProperties map[string]bool // set of properties evaluated by various keywords
|
||||
}
|
||||
|
||||
// noteIndex marks i as evaluated.
|
||||
func (a *annotations) noteIndex(i int) {
|
||||
if a.evaluatedIndexes == nil {
|
||||
a.evaluatedIndexes = map[int]bool{}
|
||||
}
|
||||
a.evaluatedIndexes[i] = true
|
||||
}
|
||||
|
||||
// noteEndIndex marks items with index less than end as evaluated.
|
||||
func (a *annotations) noteEndIndex(end int) {
|
||||
if end > a.endIndex {
|
||||
a.endIndex = end
|
||||
}
|
||||
}
|
||||
|
||||
// noteProperty marks prop as evaluated.
|
||||
func (a *annotations) noteProperty(prop string) {
|
||||
if a.evaluatedProperties == nil {
|
||||
a.evaluatedProperties = map[string]bool{}
|
||||
}
|
||||
a.evaluatedProperties[prop] = true
|
||||
}
|
||||
|
||||
// noteProperties marks all the properties in props as evaluated.
|
||||
func (a *annotations) noteProperties(props map[string]bool) {
|
||||
a.evaluatedProperties = merge(a.evaluatedProperties, props)
|
||||
}
|
||||
|
||||
// merge adds b's annotations to a.
|
||||
// a must not be nil.
|
||||
func (a *annotations) merge(b *annotations) {
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
if b.allItems {
|
||||
a.allItems = true
|
||||
}
|
||||
if b.endIndex > a.endIndex {
|
||||
a.endIndex = b.endIndex
|
||||
}
|
||||
a.evaluatedIndexes = merge(a.evaluatedIndexes, b.evaluatedIndexes)
|
||||
if b.allProperties {
|
||||
a.allProperties = true
|
||||
}
|
||||
a.evaluatedProperties = merge(a.evaluatedProperties, b.evaluatedProperties)
|
||||
}
|
||||
|
||||
// merge adds t's keys to s and returns s.
|
||||
// If s is nil, it returns a copy of t.
|
||||
func merge[K comparable](s, t map[K]bool) map[K]bool {
|
||||
if s == nil {
|
||||
return maps.Clone(t)
|
||||
}
|
||||
maps.Copy(s, t)
|
||||
return s
|
||||
}
|
||||
115
vendor/github.com/google/jsonschema-go/jsonschema/doc.go
generated
vendored
Normal file
115
vendor/github.com/google/jsonschema-go/jsonschema/doc.go
generated
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package jsonschema is an implementation of the [JSON Schema specification],
|
||||
a JSON-based format for describing the structure of JSON data.
|
||||
The package can be used to read schemas for code generation, and to validate
|
||||
data using the draft 2020-12 and draft-07 specifications. Validation with
|
||||
other drafts or custom meta-schemas is not supported.
|
||||
|
||||
Construct a [Schema] as you would any Go struct (for example, by writing
|
||||
a struct literal), or unmarshal a JSON schema into a [Schema] in the usual
|
||||
way (with [encoding/json], for instance). It can then be used for code
|
||||
generation or other purposes without further processing.
|
||||
You can also infer a schema from a Go struct.
|
||||
|
||||
# Resolution
|
||||
|
||||
A Schema can refer to other schemas, both inside and outside itself. These
|
||||
references must be resolved before a schema can be used for validation.
|
||||
Call [Schema.Resolve] to obtain a resolved schema (called a [Resolved]).
|
||||
If the schema has external references, pass a [ResolveOptions] with a [Loader]
|
||||
to load them. To validate default values in a schema, set
|
||||
[ResolveOptions.ValidateDefaults] to true.
|
||||
|
||||
# Validation
|
||||
|
||||
Call [Resolved.Validate] to validate a JSON value. The value must be a
|
||||
Go value that looks like the result of unmarshaling a JSON value into an
|
||||
[any] or a struct. For example, the JSON value
|
||||
|
||||
{"name": "Al", "scores": [90, 80, 100]}
|
||||
|
||||
could be represented as the Go value
|
||||
|
||||
map[string]any{
|
||||
"name": "Al",
|
||||
"scores": []any{90, 80, 100},
|
||||
}
|
||||
|
||||
or as a value of this type:
|
||||
|
||||
type Player struct {
|
||||
Name string `json:"name"`
|
||||
Scores []int `json:"scores"`
|
||||
}
|
||||
|
||||
# Inference
|
||||
|
||||
The [For] function returns a [Schema] describing the given Go type.
|
||||
Each field in the struct becomes a property of the schema.
|
||||
The values of "json" tags are respected: the field's property name is taken
|
||||
from the tag, and fields omitted from the JSON are omitted from the schema as
|
||||
well.
|
||||
For example, `jsonschema.For[Player]()` returns this schema:
|
||||
|
||||
{
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"scores": {
|
||||
"type": "array",
|
||||
"items": {"type": "integer"}
|
||||
}
|
||||
"required": ["name", "scores"],
|
||||
"additionalProperties": {"not": {}}
|
||||
}
|
||||
}
|
||||
|
||||
Use the "jsonschema" struct tag to provide a description for the property:
|
||||
|
||||
type Player struct {
|
||||
Name string `json:"name" jsonschema:"player name"`
|
||||
Scores []int `json:"scores" jsonschema:"scores of player's games"`
|
||||
}
|
||||
|
||||
# Deviations from the specification
|
||||
|
||||
Regular expressions are processed with Go's regexp package, which differs
|
||||
from ECMA 262, most significantly in not supporting back-references.
|
||||
See [this table of differences] for more.
|
||||
|
||||
The "format" keyword described in [section 7 of the validation spec] is recorded
|
||||
in the Schema, but is ignored during validation.
|
||||
It does not even produce [annotations].
|
||||
Use the "pattern" keyword instead: it will work more reliably across JSON Schema
|
||||
implementations. See [learnjsonschema.com] for more recommendations about "format".
|
||||
|
||||
The content keywords described in [section 8 of the validation spec]
|
||||
are recorded in the schema, but ignored during validation.
|
||||
|
||||
# Controlling behavior changes
|
||||
|
||||
Minor and patch releases of this package may introduce behavior changes as part
|
||||
of bug fixes or correctness improvements. To help manage the impact of such
|
||||
changes, the package allows you to access previous behaviors using the
|
||||
`JSONSCHEMAGODEBUG` environment variable. The available settings are listed
|
||||
below; additional options may be introduced in future releases.
|
||||
|
||||
- **typeschemasnull**: When set to `"1"`, the inferred schema for slices will
|
||||
*not* include the `null` type alongside the array type. It will also avoid
|
||||
adding `null` to non-native pointer types (such as `time.Time`). This restores
|
||||
the behavior from versions prior to v0.3.0. The default behavior is to include
|
||||
`null` in these cases.
|
||||
|
||||
[JSON Schema specification]: https://json-schema.org
|
||||
[section 7 of the validation spec]: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7
|
||||
[section 8 of the validation spec]: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.8
|
||||
[learnjsonschema.com]: https://www.learnjsonschema.com/2020-12/format-annotation/format/
|
||||
[this table of differences]: https://github.com/dlclark/regexp2?tab=readme-ov-file#compare-regexp-and-regexp2
|
||||
[annotations]: https://json-schema.org/draft/2020-12/json-schema-core#name-annotations
|
||||
*/
|
||||
package jsonschema
|
||||
400
vendor/github.com/google/jsonschema-go/jsonschema/infer.go
generated
vendored
Normal file
400
vendor/github.com/google/jsonschema-go/jsonschema/infer.go
generated
vendored
Normal file
@@ -0,0 +1,400 @@
|
||||
// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file contains functions that infer a schema from a Go type.
|
||||
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"math"
|
||||
"math/big"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
const debugEnv = "JSONSCHEMAGODEBUG"
|
||||
|
||||
// ForOptions are options for the [For] and [ForType] functions.
|
||||
type ForOptions struct {
|
||||
// If IgnoreInvalidTypes is true, fields that can't be represented as a JSON
|
||||
// Schema are ignored instead of causing an error.
|
||||
// This allows callers to adjust the resulting schema using custom knowledge.
|
||||
// For example, an interface type where all the possible implementations are
|
||||
// known can be described with "oneof".
|
||||
IgnoreInvalidTypes bool
|
||||
|
||||
// TypeSchemas maps types to their schemas.
|
||||
// If [For] encounters a type that is a key in this map, the
|
||||
// corresponding value is used as the resulting schema (after cloning to
|
||||
// ensure uniqueness).
|
||||
// Types in this map override the default translations, as described
|
||||
// in [For]'s documentation.
|
||||
// PropertyOrder defined in these schemas will not be used in [For] or [ForType].
|
||||
TypeSchemas map[reflect.Type]*Schema
|
||||
}
|
||||
|
||||
// For constructs a JSON schema object for the given type argument.
|
||||
// If non-nil, the provided options configure certain aspects of this contruction,
|
||||
// described below.
|
||||
|
||||
// It translates Go types into compatible JSON schema types, as follows.
|
||||
// These defaults can be overridden by [ForOptions.TypeSchemas].
|
||||
//
|
||||
// - Strings have schema type "string".
|
||||
// - Bools have schema type "boolean".
|
||||
// - Signed and unsigned integer types have schema type "integer".
|
||||
// - Floating point types have schema type "number".
|
||||
// - Slices and arrays have schema type "array", and a corresponding schema
|
||||
// for items.
|
||||
// - Maps with string key have schema type "object", and corresponding
|
||||
// schema for additionalProperties.
|
||||
// - Structs have schema type "object", and disallow additionalProperties.
|
||||
// Their properties are derived from exported struct fields, using the
|
||||
// struct field JSON name. Fields that are marked "omitempty" or "omitzero" are
|
||||
// considered optional; all other fields become required properties.
|
||||
// For structs, the PropertyOrder will be set to the field order.
|
||||
// - Some types in the standard library that implement json.Marshaler
|
||||
// translate to schemas that match the values to which they marshal.
|
||||
// For example, [time.Time] translates to the schema for strings.
|
||||
//
|
||||
// For will return an error if there is a cycle in the types.
|
||||
//
|
||||
// By default, For returns an error if t contains (possibly recursively) any of the
|
||||
// following Go types, as they are incompatible with the JSON schema spec.
|
||||
// If [ForOptions.IgnoreInvalidTypes] is true, then these types are ignored instead.
|
||||
// - maps with key other than 'string'
|
||||
// - function types
|
||||
// - channel types
|
||||
// - complex numbers
|
||||
// - unsafe pointers
|
||||
//
|
||||
// This function recognizes struct field tags named "jsonschema".
|
||||
// A jsonschema tag on a field is used as the description for the corresponding property.
|
||||
// For future compatibility, descriptions must not start with "WORD=", where WORD is a
|
||||
// sequence of non-whitespace characters.
|
||||
func For[T any](opts *ForOptions) (*Schema, error) {
|
||||
if opts == nil {
|
||||
opts = &ForOptions{}
|
||||
}
|
||||
schemas := maps.Clone(initialSchemaMap)
|
||||
// Add types from the options. They override the default ones.
|
||||
maps.Copy(schemas, opts.TypeSchemas)
|
||||
s, err := forType(reflect.TypeFor[T](), map[reflect.Type]bool{}, opts.IgnoreInvalidTypes, schemas)
|
||||
if err != nil {
|
||||
var z T
|
||||
return nil, fmt.Errorf("For[%T](): %w", z, err)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// ForType is like [For], but takes a [reflect.Type]
|
||||
func ForType(t reflect.Type, opts *ForOptions) (*Schema, error) {
|
||||
if opts == nil {
|
||||
opts = &ForOptions{}
|
||||
}
|
||||
schemas := maps.Clone(initialSchemaMap)
|
||||
// Add types from the options. They override the default ones.
|
||||
maps.Copy(schemas, opts.TypeSchemas)
|
||||
s, err := forType(t, map[reflect.Type]bool{}, opts.IgnoreInvalidTypes, schemas)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ForType(%s): %w", t, err)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Helper to create a *float64 pointer from a value
|
||||
func f64Ptr(f float64) *float64 {
|
||||
return &f
|
||||
}
|
||||
|
||||
func forType(t reflect.Type, seen map[reflect.Type]bool, ignore bool, schemas map[reflect.Type]*Schema) (*Schema, error) {
|
||||
// Follow pointers: the schema for *T is almost the same as for T, except that
|
||||
// an explicit JSON "null" is allowed for the pointer.
|
||||
allowNull := false
|
||||
for t.Kind() == reflect.Pointer {
|
||||
allowNull = true
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
// Check for cycles
|
||||
// User defined types have a name, so we can skip those that are natively defined
|
||||
if t.Name() != "" {
|
||||
if seen[t] {
|
||||
return nil, fmt.Errorf("cycle detected for type %v", t)
|
||||
}
|
||||
seen[t] = true
|
||||
defer delete(seen, t)
|
||||
}
|
||||
|
||||
if s := schemas[t]; s != nil {
|
||||
cloned := s.CloneSchemas()
|
||||
if os.Getenv(debugEnv) != "typeschemasnull=1" && allowNull {
|
||||
if cloned.Type != "" {
|
||||
cloned.Types = []string{"null", cloned.Type}
|
||||
cloned.Type = ""
|
||||
} else if !slices.Contains(cloned.Types, "null") {
|
||||
cloned.Types = append([]string{"null"}, cloned.Types...)
|
||||
}
|
||||
}
|
||||
return cloned, nil
|
||||
}
|
||||
|
||||
var (
|
||||
s = new(Schema)
|
||||
err error
|
||||
)
|
||||
|
||||
switch t.Kind() {
|
||||
case reflect.Bool:
|
||||
s.Type = "boolean"
|
||||
|
||||
case reflect.Int, reflect.Int64:
|
||||
s.Type = "integer"
|
||||
|
||||
case reflect.Uint, reflect.Uint64, reflect.Uintptr:
|
||||
s.Type = "integer"
|
||||
s.Minimum = f64Ptr(0)
|
||||
|
||||
case reflect.Int8:
|
||||
s.Type = "integer"
|
||||
s.Minimum = f64Ptr(math.MinInt8)
|
||||
s.Maximum = f64Ptr(math.MaxInt8)
|
||||
|
||||
case reflect.Uint8:
|
||||
s.Type = "integer"
|
||||
s.Minimum = f64Ptr(0)
|
||||
s.Maximum = f64Ptr(math.MaxUint8)
|
||||
|
||||
case reflect.Int16:
|
||||
s.Type = "integer"
|
||||
s.Minimum = f64Ptr(math.MinInt16)
|
||||
s.Maximum = f64Ptr(math.MaxInt16)
|
||||
|
||||
case reflect.Uint16:
|
||||
s.Type = "integer"
|
||||
s.Minimum = f64Ptr(0)
|
||||
s.Maximum = f64Ptr(math.MaxUint16)
|
||||
|
||||
case reflect.Int32:
|
||||
s.Type = "integer"
|
||||
s.Minimum = f64Ptr(math.MinInt32)
|
||||
s.Maximum = f64Ptr(math.MaxInt32)
|
||||
|
||||
case reflect.Uint32:
|
||||
s.Type = "integer"
|
||||
s.Minimum = f64Ptr(0)
|
||||
s.Maximum = f64Ptr(math.MaxUint32)
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
s.Type = "number"
|
||||
|
||||
case reflect.Interface:
|
||||
// Unrestricted
|
||||
|
||||
case reflect.Map:
|
||||
if t.Key().Kind() != reflect.String {
|
||||
if ignore {
|
||||
return nil, nil // ignore
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported map key type %v", t.Key().Kind())
|
||||
}
|
||||
if t.Key().Kind() != reflect.String {
|
||||
}
|
||||
s.Type = "object"
|
||||
s.AdditionalProperties, err = forType(t.Elem(), seen, ignore, schemas)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("computing map value schema: %v", err)
|
||||
}
|
||||
if ignore && s.AdditionalProperties == nil {
|
||||
// Ignore if the element type is invalid.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
case reflect.Slice, reflect.Array:
|
||||
if os.Getenv(debugEnv) != "typeschemasnull=1" && t.Kind() == reflect.Slice {
|
||||
s.Types = []string{"null", "array"}
|
||||
} else {
|
||||
s.Type = "array"
|
||||
}
|
||||
itemsSchema, err := forType(t.Elem(), seen, ignore, schemas)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("computing element schema: %v", err)
|
||||
}
|
||||
if itemsSchema == nil {
|
||||
return nil, nil
|
||||
}
|
||||
s.Items = itemsSchema
|
||||
if ignore && s.Items == nil {
|
||||
// Ignore if the element type is invalid.
|
||||
return nil, nil
|
||||
}
|
||||
if t.Kind() == reflect.Array {
|
||||
s.MinItems = Ptr(t.Len())
|
||||
s.MaxItems = Ptr(t.Len())
|
||||
}
|
||||
|
||||
case reflect.String:
|
||||
s.Type = "string"
|
||||
|
||||
case reflect.Struct:
|
||||
s.Type = "object"
|
||||
// no additional properties are allowed
|
||||
s.AdditionalProperties = falseSchema()
|
||||
|
||||
// If skipPath is non-nil, it is path to an anonymous field whose
|
||||
// schema has been replaced by a known schema.
|
||||
var skipPath []int
|
||||
for _, field := range reflect.VisibleFields(t) {
|
||||
if s.Properties == nil {
|
||||
s.Properties = make(map[string]*Schema)
|
||||
}
|
||||
if field.Anonymous {
|
||||
override := schemas[field.Type]
|
||||
if override != nil {
|
||||
// Type must be object, and only properties can be set.
|
||||
if override.Type != "object" {
|
||||
return nil, fmt.Errorf(`custom schema for embedded struct must have type "object", got %q`,
|
||||
override.Type)
|
||||
}
|
||||
// Check that all keywords relevant for objects are absent, except properties.
|
||||
ov := reflect.ValueOf(override).Elem()
|
||||
for _, sfi := range schemaFieldInfos {
|
||||
if sfi.sf.Name == "Type" || sfi.sf.Name == "Properties" {
|
||||
continue
|
||||
}
|
||||
fv := ov.FieldByIndex(sfi.sf.Index)
|
||||
if !fv.IsZero() {
|
||||
return nil, fmt.Errorf(`overrides for embedded fields can have only "Type" and "Properties"; this has %q`, sfi.sf.Name)
|
||||
}
|
||||
}
|
||||
|
||||
skipPath = field.Index
|
||||
keys := make([]string, 0, len(override.Properties))
|
||||
for k := range override.Properties {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
for _, name := range keys {
|
||||
if _, ok := s.Properties[name]; !ok {
|
||||
s.Properties[name] = override.Properties[name].CloneSchemas()
|
||||
s.PropertyOrder = append(s.PropertyOrder, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check to see if this field has been promoted from a replaced anonymous
|
||||
// type.
|
||||
if skipPath != nil {
|
||||
skip := false
|
||||
if len(field.Index) >= len(skipPath) {
|
||||
skip = true
|
||||
for i, index := range skipPath {
|
||||
if field.Index[i] != index {
|
||||
// If we're no longer in a subfield.
|
||||
skip = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if skip {
|
||||
continue
|
||||
} else {
|
||||
// Anonymous fields are followed immediately by their promoted fields.
|
||||
// Once we encounter a field that *isn't* promoted, we can stop
|
||||
// checking.
|
||||
skipPath = nil
|
||||
}
|
||||
}
|
||||
|
||||
info := fieldJSONInfo(field)
|
||||
if info.omit {
|
||||
continue
|
||||
}
|
||||
fs, err := forType(field.Type, seen, ignore, schemas)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ignore && fs == nil {
|
||||
// Skip fields of invalid type.
|
||||
continue
|
||||
}
|
||||
if tag, ok := field.Tag.Lookup("jsonschema"); ok {
|
||||
if tag == "" {
|
||||
return nil, fmt.Errorf("empty jsonschema tag on struct field %s.%s", t, field.Name)
|
||||
}
|
||||
if disallowedPrefixRegexp.MatchString(tag) {
|
||||
return nil, fmt.Errorf("tag must not begin with 'WORD=': %q", tag)
|
||||
}
|
||||
fs.Description = tag
|
||||
}
|
||||
s.Properties[info.name] = fs
|
||||
|
||||
s.PropertyOrder = append(s.PropertyOrder, info.name)
|
||||
|
||||
if !info.settings["omitempty"] && !info.settings["omitzero"] {
|
||||
s.Required = append(s.Required, info.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove PropertyOrder duplicates, keeping the last occurrence
|
||||
if len(s.PropertyOrder) > 1 {
|
||||
seen := make(map[string]bool)
|
||||
// Create a slice to hold the cleaned order (capacity = current length)
|
||||
cleaned := make([]string, 0, len(s.PropertyOrder))
|
||||
|
||||
// Iterate backwards
|
||||
for i := len(s.PropertyOrder) - 1; i >= 0; i-- {
|
||||
name := s.PropertyOrder[i]
|
||||
if !seen[name] {
|
||||
cleaned = append(cleaned, name)
|
||||
seen[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Since we collected them backwards, we need to reverse the result
|
||||
// to restore the correct order.
|
||||
slices.Reverse(cleaned)
|
||||
s.PropertyOrder = cleaned
|
||||
}
|
||||
|
||||
default:
|
||||
if ignore {
|
||||
// Ignore.
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("type %v is unsupported by jsonschema", t)
|
||||
}
|
||||
if allowNull && s.Type != "" {
|
||||
s.Types = []string{"null", s.Type}
|
||||
s.Type = ""
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// initialSchemaMap holds types from the standard library that have MarshalJSON methods.
|
||||
var initialSchemaMap = make(map[reflect.Type]*Schema)
|
||||
|
||||
func init() {
|
||||
ss := &Schema{Type: "string"}
|
||||
initialSchemaMap[reflect.TypeFor[time.Time]()] = ss
|
||||
initialSchemaMap[reflect.TypeFor[slog.Level]()] = ss
|
||||
if os.Getenv(debugEnv) == "typeschemasnull=1" {
|
||||
initialSchemaMap[reflect.TypeFor[big.Int]()] = &Schema{Types: []string{"null", "string"}}
|
||||
} else {
|
||||
initialSchemaMap[reflect.TypeFor[big.Int]()] = ss
|
||||
}
|
||||
initialSchemaMap[reflect.TypeFor[big.Rat]()] = ss
|
||||
initialSchemaMap[reflect.TypeFor[big.Float]()] = ss
|
||||
}
|
||||
|
||||
// Disallow jsonschema tag values beginning "WORD=", for future expansion.
|
||||
var disallowedPrefixRegexp = regexp.MustCompile("^[^ \t\n]*=")
|
||||
160
vendor/github.com/google/jsonschema-go/jsonschema/json_pointer.go
generated
vendored
Normal file
160
vendor/github.com/google/jsonschema-go/jsonschema/json_pointer.go
generated
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file implements JSON Pointers.
|
||||
// A JSON Pointer is a path that refers to one JSON value within another.
|
||||
// If the path is empty, it refers to the root value.
|
||||
// Otherwise, it is a sequence of slash-prefixed strings, like "/points/1/x",
|
||||
// selecting successive properties (for JSON objects) or items (for JSON arrays).
|
||||
// For example, when applied to this JSON value:
|
||||
// {
|
||||
// "points": [
|
||||
// {"x": 1, "y": 2},
|
||||
// {"x": 3, "y": 4}
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// the JSON Pointer "/points/1/x" refers to the number 3.
|
||||
// See the spec at https://datatracker.ietf.org/doc/html/rfc6901.
|
||||
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
jsonPointerEscaper = strings.NewReplacer("~", "~0", "/", "~1")
|
||||
jsonPointerUnescaper = strings.NewReplacer("~0", "~", "~1", "/")
|
||||
)
|
||||
|
||||
func escapeJSONPointerSegment(s string) string {
|
||||
return jsonPointerEscaper.Replace(s)
|
||||
}
|
||||
|
||||
func unescapeJSONPointerSegment(s string) string {
|
||||
return jsonPointerUnescaper.Replace(s)
|
||||
}
|
||||
|
||||
// parseJSONPointer splits a JSON Pointer into a sequence of segments. It doesn't
|
||||
// convert strings to numbers, because that depends on the traversal: a segment
|
||||
// is treated as a number when applied to an array, but a string when applied to
|
||||
// an object. See section 4 of the spec.
|
||||
func parseJSONPointer(ptr string) (segments []string, err error) {
|
||||
if ptr == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if ptr[0] != '/' {
|
||||
return nil, fmt.Errorf("JSON Pointer %q does not begin with '/'", ptr)
|
||||
}
|
||||
// Unlike file paths, consecutive slashes are not coalesced.
|
||||
// Split is nicer than Cut here, because it gets a final "/" right.
|
||||
segments = strings.Split(ptr[1:], "/")
|
||||
if strings.Contains(ptr, "~") {
|
||||
// Undo the simple escaping rules that allow one to include a slash in a segment.
|
||||
for i := range segments {
|
||||
segments[i] = unescapeJSONPointerSegment(segments[i])
|
||||
}
|
||||
}
|
||||
return segments, nil
|
||||
}
|
||||
|
||||
// dereferenceJSONPointer returns the Schema that sptr points to within s,
|
||||
// or an error if none.
|
||||
// This implementation suffices for JSON Schema: pointers are applied only to Schemas,
|
||||
// and refer only to Schemas.
|
||||
func dereferenceJSONPointer(s *Schema, sptr string) (_ *Schema, err error) {
|
||||
defer wrapf(&err, "JSON Pointer %q", sptr)
|
||||
|
||||
segments, err := parseJSONPointer(sptr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := reflect.ValueOf(s)
|
||||
for _, seg := range segments {
|
||||
switch v.Kind() {
|
||||
case reflect.Pointer:
|
||||
v = v.Elem()
|
||||
if !v.IsValid() {
|
||||
return nil, errors.New("navigated to nil reference")
|
||||
}
|
||||
fallthrough // if valid, can only be a pointer to a Schema
|
||||
|
||||
case reflect.Struct:
|
||||
// The segment must refer to a field in a Schema.
|
||||
if v.Type() != reflect.TypeFor[Schema]() {
|
||||
return nil, fmt.Errorf("navigated to non-Schema %s", v.Type())
|
||||
}
|
||||
v = lookupSchemaField(v, seg)
|
||||
if !v.IsValid() {
|
||||
return nil, fmt.Errorf("no schema field %q", seg)
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
// The segment must be an integer without leading zeroes that refers to an item in the
|
||||
// slice or array.
|
||||
if seg == "-" {
|
||||
return nil, errors.New("the JSON Pointer array segment '-' is not supported")
|
||||
}
|
||||
if len(seg) > 1 && seg[0] == '0' {
|
||||
return nil, fmt.Errorf("segment %q has leading zeroes", seg)
|
||||
}
|
||||
n, err := strconv.Atoi(seg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid int: %q", seg)
|
||||
}
|
||||
if n < 0 || n >= v.Len() {
|
||||
return nil, fmt.Errorf("index %d is out of bounds for array of length %d", n, v.Len())
|
||||
}
|
||||
v = v.Index(n)
|
||||
// Cannot be invalid.
|
||||
case reflect.Map:
|
||||
// The segment must be a key in the map.
|
||||
v = v.MapIndex(reflect.ValueOf(seg))
|
||||
if !v.IsValid() {
|
||||
return nil, fmt.Errorf("no key %q in map", seg)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("value %s (%s) is not a schema, slice or map", v, v.Type())
|
||||
}
|
||||
}
|
||||
if s, ok := v.Interface().(*Schema); ok {
|
||||
return s, nil
|
||||
}
|
||||
return nil, fmt.Errorf("does not refer to a schema, but to a %s", v.Type())
|
||||
}
|
||||
|
||||
// lookupSchemaField returns the value of the field with the given name in v,
|
||||
// or the zero value if there is no such field or it is not of type Schema or *Schema.
|
||||
func lookupSchemaField(v reflect.Value, name string) reflect.Value {
|
||||
if name == "type" {
|
||||
// The "type" keyword may refer to Type or Types.
|
||||
// At most one will be non-zero.
|
||||
if t := v.FieldByName("Type"); !t.IsZero() {
|
||||
return t
|
||||
}
|
||||
return v.FieldByName("Types")
|
||||
}
|
||||
if name == "items" {
|
||||
// The "items" keyword refers to the "union type" that is either a schema or a schema array.
|
||||
// Implemented using the Items representing the schema and ItemsArray for the schema array.
|
||||
if items := v.FieldByName("Items"); items.IsValid() && !items.IsNil() {
|
||||
return items
|
||||
}
|
||||
return v.FieldByName("ItemsArray")
|
||||
}
|
||||
if name == "dependencies" {
|
||||
// The "dependencies" keyword refers to both DependencyStrings and DependencySchemas maps.
|
||||
// The value on schemaFieldMap is not garanteed to be DependencySchemas which we want
|
||||
// for pointer dereference. So we use FieldByName to get the DependencySchemas map.
|
||||
return v.FieldByName("DependencySchemas")
|
||||
}
|
||||
if sf, ok := schemaFieldMap[name]; ok {
|
||||
return v.FieldByIndex(sf.Index)
|
||||
}
|
||||
return reflect.Value{}
|
||||
}
|
||||
589
vendor/github.com/google/jsonschema-go/jsonschema/resolve.go
generated
vendored
Normal file
589
vendor/github.com/google/jsonschema-go/jsonschema/resolve.go
generated
vendored
Normal file
@@ -0,0 +1,589 @@
|
||||
// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file deals with preparing a schema for validation, including various checks,
|
||||
// optimizations, and the resolution of cross-schema references.
|
||||
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A Resolved consists of a [Schema] along with associated information needed to
|
||||
// validate documents against it.
|
||||
// A Resolved has been validated against its meta-schema, and all its references
|
||||
// (the $ref and $dynamicRef keywords) have been resolved to their referenced Schemas.
|
||||
// Call [Schema.Resolve] to obtain a Resolved from a Schema.
|
||||
type Resolved struct {
|
||||
root *Schema
|
||||
draft draft
|
||||
// map from $ids to their schemas
|
||||
resolvedURIs map[string]*Schema
|
||||
// map from schemas to additional info computed during resolution
|
||||
resolvedInfos map[*Schema]*resolvedInfo
|
||||
}
|
||||
|
||||
type draft int
|
||||
|
||||
const (
|
||||
draft7 = iota
|
||||
draft2020
|
||||
)
|
||||
|
||||
func newResolved(s *Schema) *Resolved {
|
||||
return &Resolved{
|
||||
root: s,
|
||||
draft: detectDraft(s),
|
||||
resolvedURIs: map[string]*Schema{},
|
||||
resolvedInfos: map[*Schema]*resolvedInfo{},
|
||||
}
|
||||
}
|
||||
|
||||
// detectDraft inspects the raw JSON to determine the schema version.
|
||||
func detectDraft(s *Schema) draft {
|
||||
// Check explicit $schema declaration
|
||||
switch s.Schema {
|
||||
case draft7SchemaVersion, draft7SecSchemaVersion:
|
||||
return draft7
|
||||
case draft202012SchemaVersion:
|
||||
return draft2020
|
||||
default:
|
||||
// If nothing matches default to the latest supported version.
|
||||
return draft2020
|
||||
}
|
||||
}
|
||||
|
||||
// resolvedInfo holds information specific to a schema that is computed by [Schema.Resolve].
|
||||
type resolvedInfo struct {
|
||||
s *Schema
|
||||
// The JSON Pointer path from the root schema to here.
|
||||
// Used in errors.
|
||||
path string
|
||||
// The schema's base schema.
|
||||
// If the schema is the root or has an ID, its base is itself.
|
||||
// Otherwise, its base is the innermost enclosing schema whose base
|
||||
// is itself.
|
||||
// Intuitively, a base schema is one that can be referred to with a
|
||||
// fragmentless URI.
|
||||
base *Schema
|
||||
// The URI for the schema, if it is the root or has an ID.
|
||||
// Otherwise nil.
|
||||
// Invariants:
|
||||
// s.base.uri != nil.
|
||||
// s.base == s <=> s.uri != nil
|
||||
uri *url.URL
|
||||
// The schema to which Ref refers.
|
||||
resolvedRef *Schema
|
||||
|
||||
// If the schema has a dynamic ref, exactly one of the next two fields
|
||||
// will be non-zero after successful resolution.
|
||||
// The schema to which the dynamic ref refers when it acts lexically.
|
||||
resolvedDynamicRef *Schema
|
||||
// The anchor to look up on the stack when the dynamic ref acts dynamically.
|
||||
dynamicRefAnchor string
|
||||
|
||||
// The following fields are independent of arguments to Schema.Resolved,
|
||||
// so they could live on the Schema. We put them here for simplicity.
|
||||
|
||||
// The set of required properties.
|
||||
isRequired map[string]bool
|
||||
|
||||
// Compiled regexps.
|
||||
pattern *regexp.Regexp
|
||||
patternProperties map[*regexp.Regexp]*Schema
|
||||
|
||||
// Map from anchors to subschemas.
|
||||
anchors map[string]anchorInfo
|
||||
}
|
||||
|
||||
// Schema returns the schema that was resolved.
|
||||
// It must not be modified.
|
||||
func (r *Resolved) Schema() *Schema { return r.root }
|
||||
|
||||
// schemaString returns a short string describing the schema.
|
||||
func (r *Resolved) schemaString(s *Schema) string {
|
||||
if s.ID != "" {
|
||||
return s.ID
|
||||
}
|
||||
info := r.resolvedInfos[s]
|
||||
if info.path != "" {
|
||||
return info.path
|
||||
}
|
||||
return "<anonymous schema>"
|
||||
}
|
||||
|
||||
// A Loader reads and unmarshals the schema at uri, if any.
|
||||
type Loader func(uri *url.URL) (*Schema, error)
|
||||
|
||||
// ResolveOptions are options for [Schema.Resolve].
|
||||
type ResolveOptions struct {
|
||||
// BaseURI is the URI relative to which the root schema should be resolved.
|
||||
// If non-empty, must be an absolute URI (one that starts with a scheme).
|
||||
// It is resolved (in the URI sense; see [url.ResolveReference]) with root's
|
||||
// $id property.
|
||||
// If the resulting URI is not absolute, then the schema cannot contain
|
||||
// relative URI references.
|
||||
BaseURI string
|
||||
// Loader loads schemas that are referred to by a $ref but are not under the
|
||||
// root schema (remote references).
|
||||
// If nil, resolving a remote reference will return an error.
|
||||
Loader Loader
|
||||
// ValidateDefaults determines whether to validate values of "default" keywords
|
||||
// against their schemas.
|
||||
// The [JSON Schema specification] does not require this, but it is recommended
|
||||
// if defaults will be used.
|
||||
//
|
||||
// [JSON Schema specification]: https://json-schema.org/understanding-json-schema/reference/annotations
|
||||
ValidateDefaults bool
|
||||
}
|
||||
|
||||
// Resolve resolves all references within the schema and performs other tasks that
|
||||
// prepare the schema for validation.
|
||||
// If opts is nil, the default values are used.
|
||||
// The schema must not be changed after Resolve is called.
|
||||
// The same schema may be resolved multiple times.
|
||||
func (root *Schema) Resolve(opts *ResolveOptions) (*Resolved, error) {
|
||||
// There are up to five steps required to prepare a schema to validate.
|
||||
// 1. Load: read the schema from somewhere and unmarshal it.
|
||||
// This schema (root) may have been loaded or created in memory, but other schemas that
|
||||
// come into the picture in step 4 will be loaded by the given loader.
|
||||
// 2. Check: validate the schema against a meta-schema, and perform other well-formedness checks.
|
||||
// Precompute some values along the way.
|
||||
// 3. Resolve URIs: determine the base URI of the root and all its subschemas, and
|
||||
// resolve (in the URI sense) all identifiers and anchors with their bases. This step results
|
||||
// in a map from URIs to schemas within root.
|
||||
// 4. Resolve references: all refs in the schemas are replaced with the schema they refer to.
|
||||
// 5. (Optional.) If opts.ValidateDefaults is true, validate the defaults.
|
||||
r := &resolver{loaded: map[string]*Resolved{}}
|
||||
if opts != nil {
|
||||
r.opts = *opts
|
||||
}
|
||||
var base *url.URL
|
||||
if r.opts.BaseURI == "" {
|
||||
base = &url.URL{} // so we can call ResolveReference on it
|
||||
} else {
|
||||
var err error
|
||||
base, err = url.Parse(r.opts.BaseURI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing base URI: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if r.opts.Loader == nil {
|
||||
r.opts.Loader = func(uri *url.URL) (*Schema, error) {
|
||||
return nil, errors.New("cannot resolve remote schemas: no loader passed to Schema.Resolve")
|
||||
}
|
||||
}
|
||||
|
||||
resolved, err := r.resolve(root, base)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.opts.ValidateDefaults {
|
||||
if err := resolved.validateDefaults(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// TODO: before we return, throw away anything we don't need for validation.
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
// A resolver holds the state for resolution.
|
||||
type resolver struct {
|
||||
opts ResolveOptions
|
||||
// A cache of loaded and partly resolved schemas. (They may not have had their
|
||||
// refs resolved.) The cache ensures that the loader will never be called more
|
||||
// than once with the same URI, and that reference cycles are handled properly.
|
||||
loaded map[string]*Resolved
|
||||
}
|
||||
|
||||
func (r *resolver) resolve(s *Schema, baseURI *url.URL) (*Resolved, error) {
|
||||
if baseURI.Fragment != "" {
|
||||
return nil, fmt.Errorf("base URI %s must not have a fragment", baseURI)
|
||||
}
|
||||
rs := newResolved(s)
|
||||
|
||||
if err := s.check(rs.resolvedInfos); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := resolveURIs(rs, baseURI); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Remember the schema by both the URI we loaded it from and its canonical name,
|
||||
// which may differ if the schema has an $id.
|
||||
// We must set the map before calling resolveRefs, or ref cycles will cause unbounded recursion.
|
||||
r.loaded[baseURI.String()] = rs
|
||||
r.loaded[rs.resolvedInfos[s].uri.String()] = rs
|
||||
|
||||
if err := r.resolveRefs(rs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func (root *Schema) check(infos map[*Schema]*resolvedInfo) error {
|
||||
// Check for structural validity. Do this first and fail fast:
|
||||
// bad structure will cause other code to panic.
|
||||
if err := root.checkStructure(infos); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errs []error
|
||||
report := func(err error) { errs = append(errs, err) }
|
||||
|
||||
for ss := range root.all() {
|
||||
ss.checkLocal(report, infos)
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// checkStructure verifies that root and its subschemas form a tree.
|
||||
// It also assigns each schema a unique path, to improve error messages.
|
||||
func (root *Schema) checkStructure(infos map[*Schema]*resolvedInfo) error {
|
||||
assert(len(infos) == 0, "non-empty infos")
|
||||
|
||||
var check func(reflect.Value, []byte) error
|
||||
check = func(v reflect.Value, path []byte) error {
|
||||
// For the purpose of error messages, the root schema has path "root"
|
||||
// and other schemas' paths are their JSON Pointer from the root.
|
||||
p := "root"
|
||||
if len(path) > 0 {
|
||||
p = string(path)
|
||||
}
|
||||
s := v.Interface().(*Schema)
|
||||
if s == nil {
|
||||
return fmt.Errorf("jsonschema: schema at %s is nil", p)
|
||||
}
|
||||
if info, ok := infos[s]; ok {
|
||||
// We've seen s before.
|
||||
// The schema graph at root is not a tree, but it needs to
|
||||
// be because a schema's base must be unique.
|
||||
// A cycle would also put Schema.all into an infinite recursion.
|
||||
return fmt.Errorf("jsonschema: schemas at %s do not form a tree; %s appears more than once (also at %s)",
|
||||
root, info.path, p)
|
||||
}
|
||||
infos[s] = &resolvedInfo{s: s, path: p}
|
||||
|
||||
for _, info := range schemaFieldInfos {
|
||||
fv := v.Elem().FieldByIndex(info.sf.Index)
|
||||
switch info.sf.Type {
|
||||
case schemaType:
|
||||
// A field that contains an individual schema.
|
||||
// A nil is valid: it just means the field isn't present.
|
||||
if !fv.IsNil() {
|
||||
if err := check(fv, fmt.Appendf(path, "/%s", info.jsonName)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case schemaSliceType:
|
||||
for i := range fv.Len() {
|
||||
if err := check(fv.Index(i), fmt.Appendf(path, "/%s/%d", info.jsonName, i)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case schemaMapType:
|
||||
iter := fv.MapRange()
|
||||
for iter.Next() {
|
||||
key := escapeJSONPointerSegment(iter.Key().String())
|
||||
if err := check(iter.Value(), fmt.Appendf(path, "/%s/%s", info.jsonName, key)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return check(reflect.ValueOf(root), make([]byte, 0, 256))
|
||||
}
|
||||
|
||||
// checkLocal checks s for validity, independently of other schemas it may refer to.
|
||||
// Since checking a regexp involves compiling it, checkLocal saves those compiled regexps
|
||||
// in the schema for later use.
|
||||
// It appends the errors it finds to errs.
|
||||
func (s *Schema) checkLocal(report func(error), infos map[*Schema]*resolvedInfo) {
|
||||
addf := func(format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
report(fmt.Errorf("jsonschema.Schema: %s: %s", s, msg))
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
addf("nil subschema")
|
||||
return
|
||||
}
|
||||
if err := s.basicChecks(); err != nil {
|
||||
report(err)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: validate the schema's properties,
|
||||
// ideally by jsonschema-validating it against the meta-schema.
|
||||
|
||||
// Some properties are present so that Schemas can round-trip, but we do not
|
||||
// validate them.
|
||||
// Currently, it's just the $vocabulary property.
|
||||
// As a special case, we can validate the 2020-12 meta-schema.
|
||||
if s.Vocabulary != nil && s.Schema != draft202012SchemaVersion {
|
||||
addf("cannot validate a schema with $vocabulary")
|
||||
}
|
||||
|
||||
info := infos[s]
|
||||
|
||||
// Check and compile regexps.
|
||||
if s.Pattern != "" {
|
||||
re, err := regexp.Compile(s.Pattern)
|
||||
if err != nil {
|
||||
addf("pattern: %v", err)
|
||||
} else {
|
||||
info.pattern = re
|
||||
}
|
||||
}
|
||||
if len(s.PatternProperties) > 0 {
|
||||
info.patternProperties = map[*regexp.Regexp]*Schema{}
|
||||
for reString, subschema := range s.PatternProperties {
|
||||
re, err := regexp.Compile(reString)
|
||||
if err != nil {
|
||||
addf("patternProperties[%q]: %v", reString, err)
|
||||
continue
|
||||
}
|
||||
info.patternProperties[re] = subschema
|
||||
}
|
||||
}
|
||||
|
||||
// Build a set of required properties, to avoid quadratic behavior when validating
|
||||
// a struct.
|
||||
if len(s.Required) > 0 {
|
||||
info.isRequired = map[string]bool{}
|
||||
for _, r := range s.Required {
|
||||
info.isRequired[r] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// resolveURIs resolves the ids and anchors in all the schemas of root, relative
|
||||
// to baseURI.
|
||||
// See https://json-schema.org/draft/2020-12/json-schema-core#section-8.2, section
|
||||
// 8.2.1.
|
||||
//
|
||||
// Every schema has a base URI and a parent base URI.
|
||||
//
|
||||
// The parent base URI is the base URI of the lexically enclosing schema, or for
|
||||
// a root schema, the URI it was loaded from or the one supplied to [Schema.Resolve].
|
||||
//
|
||||
// If the schema has no $id property, the base URI of a schema is that of its parent.
|
||||
// If the schema does have an $id, it must be a URI, possibly relative. The schema's
|
||||
// base URI is the $id resolved (in the sense of [url.URL.ResolveReference]) against
|
||||
// the parent base.
|
||||
//
|
||||
// As an example, consider this schema loaded from http://a.com/root.json (quotes omitted):
|
||||
//
|
||||
// {
|
||||
// allOf: [
|
||||
// {$id: "sub1.json", minLength: 5},
|
||||
// {$id: "http://b.com", minimum: 10},
|
||||
// {not: {maximum: 20}}
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// The base URIs are as follows. Schema locations are expressed in the JSON Pointer notation.
|
||||
//
|
||||
// schema base URI
|
||||
// root http://a.com/root.json
|
||||
// allOf/0 http://a.com/sub1.json
|
||||
// allOf/1 http://b.com (absolute $id; doesn't matter that it's not under the loaded URI)
|
||||
// allOf/2 http://a.com/root.json (inherited from parent)
|
||||
// allOf/2/not http://a.com/root.json (inherited from parent)
|
||||
func resolveURIs(rs *Resolved, baseURI *url.URL) error {
|
||||
// Anchors and dynamic anchors are URI fragments that are scoped to their base.
|
||||
// We treat them as keys in a map stored within the schema.
|
||||
setAnchor := func(s *Schema, baseInfo *resolvedInfo, anchor string, dynamic bool) error {
|
||||
if anchor != "" {
|
||||
if _, ok := baseInfo.anchors[anchor]; ok {
|
||||
return fmt.Errorf("duplicate anchor %q in %s", anchor, baseInfo.uri)
|
||||
}
|
||||
if baseInfo.anchors == nil {
|
||||
baseInfo.anchors = map[string]anchorInfo{}
|
||||
}
|
||||
baseInfo.anchors[anchor] = anchorInfo{s, dynamic}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var resolve func(s, base *Schema) error
|
||||
resolve = func(s, base *Schema) error {
|
||||
info := rs.resolvedInfos[s]
|
||||
baseInfo := rs.resolvedInfos[base]
|
||||
|
||||
// ids are scoped to the root.
|
||||
if s.ID != "" {
|
||||
// draft-7 specific
|
||||
// https://json-schema.org/draft-07/draft-handrews-json-schema-01#rfc.section.8.3
|
||||
// "All other properties in a "$ref" object MUST be ignored."
|
||||
ignore := rs.draft == draft7 && s.Ref != ""
|
||||
if !ignore {
|
||||
// A non-empty ID establishes a new base.
|
||||
idURI, err := url.Parse(s.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rs.draft == draft2020 && idURI.Fragment != "" {
|
||||
return fmt.Errorf("$id %s must not have a fragment", s.ID)
|
||||
}
|
||||
if rs.draft == draft7 && idURI.Fragment != "" {
|
||||
// anchor did not exist in draft 7, id was used for base uri and document navigation
|
||||
// https://json-schema.org/draft-07/draft-handrews-json-schema-01#id-keyword
|
||||
anchorName := strings.TrimPrefix(s.ID, "#")
|
||||
setAnchor(s, baseInfo, anchorName, false)
|
||||
} else {
|
||||
// The base URI for this schema is its $id resolved against the parent base.
|
||||
info.uri = baseInfo.uri.ResolveReference(idURI)
|
||||
if !info.uri.IsAbs() {
|
||||
return fmt.Errorf("$id %s does not resolve to an absolute URI (base is %q)", s.ID, baseInfo.uri)
|
||||
}
|
||||
rs.resolvedURIs[info.uri.String()] = s
|
||||
base = s // needed for anchors
|
||||
baseInfo = rs.resolvedInfos[base]
|
||||
}
|
||||
}
|
||||
}
|
||||
info.base = base
|
||||
if rs.draft == draft2020 {
|
||||
setAnchor(s, baseInfo, s.Anchor, false)
|
||||
setAnchor(s, baseInfo, s.DynamicAnchor, true)
|
||||
}
|
||||
|
||||
for c := range s.children() {
|
||||
if err := resolve(c, base); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set the root URI to the base for now. If the root has an $id, this will change.
|
||||
rs.resolvedInfos[rs.root].uri = baseURI
|
||||
// The original base, even if changed, is still a valid way to refer to the root.
|
||||
rs.resolvedURIs[baseURI.String()] = rs.root
|
||||
|
||||
return resolve(rs.root, rs.root)
|
||||
}
|
||||
|
||||
// resolveRefs replaces every ref in the schemas with the schema it refers to.
|
||||
// A reference that doesn't resolve within the schema may refer to some other schema
|
||||
// that needs to be loaded.
|
||||
func (r *resolver) resolveRefs(rs *Resolved) error {
|
||||
for s := range rs.root.all() {
|
||||
info := rs.resolvedInfos[s]
|
||||
if s.Ref != "" {
|
||||
refSchema, _, err := r.resolveRef(rs, s, s.Ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Whether or not the anchor referred to by $ref fragment is dynamic,
|
||||
// the ref still treats it lexically.
|
||||
info.resolvedRef = refSchema
|
||||
}
|
||||
if s.DynamicRef != "" {
|
||||
refSchema, frag, err := r.resolveRef(rs, s, s.DynamicRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if frag != "" {
|
||||
// The dynamic ref's fragment points to a dynamic anchor.
|
||||
// We must resolve the fragment at validation time.
|
||||
info.dynamicRefAnchor = frag
|
||||
} else {
|
||||
// There is no dynamic anchor in the lexically referenced schema,
|
||||
// so the dynamic ref behaves like a lexical ref.
|
||||
info.resolvedDynamicRef = refSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveRef resolves the reference ref, which is either s.Ref or s.DynamicRef.
|
||||
func (r *resolver) resolveRef(rs *Resolved, s *Schema, ref string) (_ *Schema, dynamicFragment string, err error) {
|
||||
refURI, err := url.Parse(ref)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
// URI-resolve the ref against the current base URI to get a complete URI.
|
||||
base := rs.resolvedInfos[s].base
|
||||
refURI = rs.resolvedInfos[base].uri.ResolveReference(refURI)
|
||||
// The non-fragment part of a ref URI refers to the base URI of some schema.
|
||||
// This part is the same for dynamic refs too: their non-fragment part resolves
|
||||
// lexically.
|
||||
u := *refURI
|
||||
u.Fragment = ""
|
||||
fraglessRefURI := &u
|
||||
// Look it up locally.
|
||||
referencedSchema := rs.resolvedURIs[fraglessRefURI.String()]
|
||||
if referencedSchema == nil {
|
||||
// The schema is remote. Maybe we've already loaded it.
|
||||
// We assume that the non-fragment part of refURI refers to a top-level schema
|
||||
// document. That is, we don't support the case exemplified by
|
||||
// http://foo.com/bar.json/baz, where the document is in bar.json and
|
||||
// the reference points to a subschema within it.
|
||||
// TODO: support that case.
|
||||
if lrs := r.loaded[fraglessRefURI.String()]; lrs != nil {
|
||||
referencedSchema = lrs.root
|
||||
} else {
|
||||
// Try to load the schema.
|
||||
ls, err := r.opts.Loader(fraglessRefURI)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("loading %s: %w", fraglessRefURI, err)
|
||||
}
|
||||
// Check if referenced schema has $schema defined. If not it should inherit the resolved
|
||||
if ls.Schema == "" {
|
||||
ls.Schema = s.Schema
|
||||
}
|
||||
lrs, err := r.resolve(ls, fraglessRefURI)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
referencedSchema = lrs.root
|
||||
assert(referencedSchema != nil, "nil referenced schema")
|
||||
// Copy the resolvedInfos from lrs into rs, without overwriting
|
||||
// (hence we can't use maps.Insert).
|
||||
for s, i := range lrs.resolvedInfos {
|
||||
if rs.resolvedInfos[s] == nil {
|
||||
rs.resolvedInfos[s] = i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frag := refURI.Fragment
|
||||
// Look up frag in refSchema.
|
||||
// frag is either a JSON Pointer or the name of an anchor.
|
||||
// A JSON Pointer is either the empty string or begins with a '/',
|
||||
// whereas anchors are always non-empty strings that don't contain slashes.
|
||||
if frag != "" && !strings.HasPrefix(frag, "/") {
|
||||
resInfo := rs.resolvedInfos[referencedSchema]
|
||||
info, found := resInfo.anchors[frag]
|
||||
|
||||
if !found {
|
||||
return nil, "", fmt.Errorf("no anchor %q in %s", frag, s)
|
||||
}
|
||||
if info.dynamic {
|
||||
dynamicFragment = frag
|
||||
}
|
||||
return info.schema, dynamicFragment, nil
|
||||
}
|
||||
// frag is a JSON Pointer.
|
||||
s, err = dereferenceJSONPointer(referencedSchema, frag)
|
||||
return s, "", err
|
||||
}
|
||||
642
vendor/github.com/google/jsonschema-go/jsonschema/schema.go
generated
vendored
Normal file
642
vendor/github.com/google/jsonschema-go/jsonschema/schema.go
generated
vendored
Normal file
@@ -0,0 +1,642 @@
|
||||
// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
"maps"
|
||||
"math"
|
||||
"reflect"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// A Schema is a JSON schema object.
|
||||
// It supports both draft-07 and the 2020-12 draft specifications:
|
||||
// - Draft-07: https://json-schema.org/draft-07/draft-handrews-json-schema-01
|
||||
// and https://json-schema.org/draft-07/draft-handrews-json-schema-validation-01
|
||||
// - Draft 2020-12: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01
|
||||
// and https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01
|
||||
//
|
||||
// A Schema value may have non-zero values for more than one field:
|
||||
// all relevant non-zero fields are used for validation.
|
||||
// There is one exception to provide more Go type-safety: the Type and Types fields
|
||||
// are mutually exclusive.
|
||||
//
|
||||
// Since this struct is a Go representation of a JSON value, it inherits JSON's
|
||||
// distinction between nil and empty. Nil slices and maps are considered absent,
|
||||
// but empty ones are present and affect validation. For example,
|
||||
//
|
||||
// Schema{Enum: nil}
|
||||
//
|
||||
// is equivalent to an empty schema, so it validates every instance. But
|
||||
//
|
||||
// Schema{Enum: []any{}}
|
||||
//
|
||||
// requires equality to some slice element, so it vacuously rejects every instance.
|
||||
type Schema struct {
|
||||
// core
|
||||
ID string `json:"$id,omitempty"`
|
||||
Schema string `json:"$schema,omitempty"`
|
||||
Ref string `json:"$ref,omitempty"`
|
||||
Comment string `json:"$comment,omitempty"`
|
||||
Defs map[string]*Schema `json:"$defs,omitempty"`
|
||||
Definitions map[string]*Schema `json:"definitions,omitempty"`
|
||||
|
||||
// split draft 7 Dependencies into DependencySchemas and DependencyStrings
|
||||
DependencySchemas map[string]*Schema `json:"-"`
|
||||
DependencyStrings map[string][]string `json:"-"`
|
||||
|
||||
Anchor string `json:"$anchor,omitempty"`
|
||||
DynamicAnchor string `json:"$dynamicAnchor,omitempty"`
|
||||
DynamicRef string `json:"$dynamicRef,omitempty"`
|
||||
Vocabulary map[string]bool `json:"$vocabulary,omitempty"`
|
||||
|
||||
// metadata
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Default json.RawMessage `json:"default,omitempty"`
|
||||
Deprecated bool `json:"deprecated,omitempty"`
|
||||
ReadOnly bool `json:"readOnly,omitempty"`
|
||||
WriteOnly bool `json:"writeOnly,omitempty"`
|
||||
Examples []any `json:"examples,omitempty"`
|
||||
|
||||
// validation
|
||||
// Use Type for a single type, or Types for multiple types; never both.
|
||||
Type string `json:"-"`
|
||||
Types []string `json:"-"`
|
||||
Enum []any `json:"enum,omitempty"`
|
||||
// Const is *any because a JSON null (Go nil) is a valid value.
|
||||
Const *any `json:"const,omitempty"`
|
||||
MultipleOf *float64 `json:"multipleOf,omitempty"`
|
||||
Minimum *float64 `json:"minimum,omitempty"`
|
||||
Maximum *float64 `json:"maximum,omitempty"`
|
||||
ExclusiveMinimum *float64 `json:"exclusiveMinimum,omitempty"`
|
||||
ExclusiveMaximum *float64 `json:"exclusiveMaximum,omitempty"`
|
||||
MinLength *int `json:"minLength,omitempty"`
|
||||
MaxLength *int `json:"maxLength,omitempty"`
|
||||
Pattern string `json:"pattern,omitempty"`
|
||||
|
||||
// arrays
|
||||
PrefixItems []*Schema `json:"prefixItems,omitempty"`
|
||||
Items *Schema `json:"-"`
|
||||
ItemsArray []*Schema `json:"-"`
|
||||
MinItems *int `json:"minItems,omitempty"`
|
||||
MaxItems *int `json:"maxItems,omitempty"`
|
||||
AdditionalItems *Schema `json:"additionalItems,omitempty"`
|
||||
UniqueItems bool `json:"uniqueItems,omitempty"`
|
||||
Contains *Schema `json:"contains,omitempty"`
|
||||
MinContains *int `json:"minContains,omitempty"` // *int, not int: default is 1, not 0
|
||||
MaxContains *int `json:"maxContains,omitempty"`
|
||||
UnevaluatedItems *Schema `json:"unevaluatedItems,omitempty"`
|
||||
|
||||
// objects
|
||||
MinProperties *int `json:"minProperties,omitempty"`
|
||||
MaxProperties *int `json:"maxProperties,omitempty"`
|
||||
Required []string `json:"required,omitempty"`
|
||||
DependentRequired map[string][]string `json:"dependentRequired,omitempty"`
|
||||
Properties map[string]*Schema `json:"properties,omitempty"`
|
||||
PatternProperties map[string]*Schema `json:"patternProperties,omitempty"`
|
||||
AdditionalProperties *Schema `json:"additionalProperties,omitempty"`
|
||||
PropertyNames *Schema `json:"propertyNames,omitempty"`
|
||||
UnevaluatedProperties *Schema `json:"unevaluatedProperties,omitempty"`
|
||||
|
||||
// logic
|
||||
AllOf []*Schema `json:"allOf,omitempty"`
|
||||
AnyOf []*Schema `json:"anyOf,omitempty"`
|
||||
OneOf []*Schema `json:"oneOf,omitempty"`
|
||||
Not *Schema `json:"not,omitempty"`
|
||||
|
||||
// conditional
|
||||
If *Schema `json:"if,omitempty"`
|
||||
Then *Schema `json:"then,omitempty"`
|
||||
Else *Schema `json:"else,omitempty"`
|
||||
DependentSchemas map[string]*Schema `json:"dependentSchemas,omitempty"`
|
||||
|
||||
// other
|
||||
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.8
|
||||
ContentEncoding string `json:"contentEncoding,omitempty"`
|
||||
ContentMediaType string `json:"contentMediaType,omitempty"`
|
||||
ContentSchema *Schema `json:"contentSchema,omitempty"`
|
||||
|
||||
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7
|
||||
Format string `json:"format,omitempty"`
|
||||
|
||||
// Extra allows for additional keywords beyond those specified.
|
||||
Extra map[string]any `json:"-"`
|
||||
|
||||
// PropertyOrder records the ordering of properties for JSON rendering.
|
||||
//
|
||||
// During [For], PropertyOrder is set to the field order,
|
||||
// if the type used for inference is a struct.
|
||||
//
|
||||
// If PropertyOrder is set, it controls the relative ordering of properties in [Schema.MarshalJSON].
|
||||
// The rendered JSON first lists any properties that appear in the PropertyOrder slice in the order
|
||||
// they appear, followed by all other properties that do not appear in the PropertyOrder slice in an
|
||||
// undefined but deterministic order.
|
||||
PropertyOrder []string `json:"-"`
|
||||
}
|
||||
|
||||
// falseSchema returns a new Schema tree that fails to validate any value.
|
||||
func falseSchema() *Schema {
|
||||
return &Schema{Not: &Schema{}}
|
||||
}
|
||||
|
||||
// anchorInfo records the subschema to which an anchor refers, and whether
|
||||
// the anchor keyword is $anchor or $dynamicAnchor.
|
||||
type anchorInfo struct {
|
||||
schema *Schema
|
||||
dynamic bool
|
||||
}
|
||||
|
||||
// String returns a short description of the schema.
|
||||
func (s *Schema) String() string {
|
||||
if s.ID != "" {
|
||||
return s.ID
|
||||
}
|
||||
if a := cmp.Or(s.Anchor, s.DynamicAnchor); a != "" {
|
||||
return fmt.Sprintf("anchor %s", a)
|
||||
}
|
||||
return "<anonymous schema>"
|
||||
}
|
||||
|
||||
// CloneSchemas returns a copy of s.
|
||||
// The copy is shallow except for sub-schemas, which are themelves copied with CloneSchemas.
|
||||
// This allows both s and s.CloneSchemas() to appear as sub-schemas of the same parent.
|
||||
func (s *Schema) CloneSchemas() *Schema {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
s2 := *s
|
||||
v := reflect.ValueOf(&s2)
|
||||
for _, info := range schemaFieldInfos {
|
||||
fv := v.Elem().FieldByIndex(info.sf.Index)
|
||||
switch info.sf.Type {
|
||||
case schemaType:
|
||||
sscss := fv.Interface().(*Schema)
|
||||
fv.Set(reflect.ValueOf(sscss.CloneSchemas()))
|
||||
|
||||
case schemaSliceType:
|
||||
slice := fv.Interface().([]*Schema)
|
||||
slice = slices.Clone(slice)
|
||||
for i, ss := range slice {
|
||||
slice[i] = ss.CloneSchemas()
|
||||
}
|
||||
fv.Set(reflect.ValueOf(slice))
|
||||
|
||||
case schemaMapType:
|
||||
m := fv.Interface().(map[string]*Schema)
|
||||
m = maps.Clone(m)
|
||||
for k, ss := range m {
|
||||
m[k] = ss.CloneSchemas()
|
||||
}
|
||||
fv.Set(reflect.ValueOf(m))
|
||||
|
||||
}
|
||||
}
|
||||
return &s2
|
||||
}
|
||||
|
||||
func (s *Schema) basicChecks() error {
|
||||
if s.Type != "" && s.Types != nil {
|
||||
return errors.New("both Type and Types are set; at most one should be")
|
||||
}
|
||||
if s.Defs != nil && s.Definitions != nil {
|
||||
return errors.New("both Defs and Definitions are set; at most one should be")
|
||||
}
|
||||
if s.Items != nil && s.ItemsArray != nil {
|
||||
return errors.New("both Items and ItemsArray are set; at most one should be")
|
||||
}
|
||||
propertyOrderSeen := make(map[string]bool)
|
||||
for _, val := range s.PropertyOrder {
|
||||
if _, ok := propertyOrderSeen[val]; ok {
|
||||
// Duplicate found
|
||||
return fmt.Errorf("property order slice cannot contain duplicate entries, found duplicate %q", val)
|
||||
}
|
||||
propertyOrderSeen[val] = true
|
||||
}
|
||||
|
||||
for key := range s.DependencySchemas {
|
||||
// Check if the key exists in the dependency strings map
|
||||
if _, exists := s.DependencyStrings[key]; exists {
|
||||
return fmt.Errorf("dependency key %q cannot be defined as both a schema and a string array", key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type schemaWithoutMethods Schema // doesn't implement json.{Unm,M}arshaler
|
||||
|
||||
func (s Schema) MarshalJSON() ([]byte, error) {
|
||||
// NOTE: Use a value receiver here to avoid the encoding/json bugs
|
||||
// described in golang/go#22967, golang/go#33993, and golang/go#55890.
|
||||
// With a pointer receiver, MarshalJSON is only called for Schema in
|
||||
// some cases (for example when the field value is addressable, or not
|
||||
// stored as a map value), which leads to inconsistent JSON encoding.
|
||||
// A value receiver makes Schema itself implement json.Marshaler and
|
||||
// ensures that encoding/json always calls this method.
|
||||
if err := s.basicChecks(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Marshal either Type or Types as "type".
|
||||
var typ any
|
||||
switch {
|
||||
case s.Type != "":
|
||||
typ = s.Type
|
||||
case s.Types != nil:
|
||||
typ = s.Types
|
||||
}
|
||||
|
||||
var items any
|
||||
switch {
|
||||
case s.Items != nil:
|
||||
items = s.Items
|
||||
case s.ItemsArray != nil:
|
||||
items = s.ItemsArray
|
||||
}
|
||||
|
||||
var dep map[string]any
|
||||
size := len(s.DependencySchemas) + len(s.DependencyStrings)
|
||||
if size > 0 {
|
||||
dep = make(map[string]any, size)
|
||||
for k, v := range s.DependencySchemas {
|
||||
dep[k] = v
|
||||
}
|
||||
for k, v := range s.DependencyStrings {
|
||||
dep[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
ms := struct {
|
||||
Type any `json:"type,omitempty"`
|
||||
Properties json.Marshaler `json:"properties,omitempty"`
|
||||
Dependencies map[string]any `json:"dependencies,omitempty"`
|
||||
Items any `json:"items,omitempty"`
|
||||
*schemaWithoutMethods
|
||||
}{
|
||||
Type: typ,
|
||||
Dependencies: dep,
|
||||
Items: items,
|
||||
schemaWithoutMethods: (*schemaWithoutMethods)(&s),
|
||||
}
|
||||
// Marshal properties, even if the empty map (but not nil).
|
||||
if s.Properties != nil {
|
||||
ms.Properties = orderedProperties{
|
||||
props: s.Properties,
|
||||
order: s.PropertyOrder,
|
||||
}
|
||||
}
|
||||
|
||||
bs, err := marshalStructWithMap(&ms, "Extra")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Marshal {} as true and {"not": {}} as false.
|
||||
// It is wasteful to do this here instead of earlier, but much easier.
|
||||
switch {
|
||||
case bytes.Equal(bs, []byte(`{}`)):
|
||||
bs = []byte("true")
|
||||
case bytes.Equal(bs, []byte(`{"not":true}`)):
|
||||
bs = []byte("false")
|
||||
}
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
// orderedProperties is a helper to marshal the properties map in a specific order.
|
||||
type orderedProperties struct {
|
||||
props map[string]*Schema
|
||||
order []string
|
||||
}
|
||||
|
||||
func (op orderedProperties) MarshalJSON() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte('{')
|
||||
|
||||
first := true
|
||||
processed := make(map[string]bool, len(op.props))
|
||||
|
||||
// Helper closure to write "key": value
|
||||
writeEntry := func(key string, val *Schema) error {
|
||||
if !first {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
first = false
|
||||
|
||||
// Marshal the Key
|
||||
keyBytes, err := json.Marshal(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf.Write(keyBytes)
|
||||
|
||||
buf.WriteByte(':')
|
||||
|
||||
// Marshal the Value
|
||||
valBytes, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf.Write(valBytes)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write keys explicitly listed in PropertyOrder
|
||||
for _, name := range op.order {
|
||||
if prop, ok := op.props[name]; ok {
|
||||
if err := writeEntry(name, prop); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
processed[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Write any remaining keys
|
||||
var remaining []string
|
||||
for name := range op.props {
|
||||
if !processed[name] {
|
||||
remaining = append(remaining, name)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the slice alphabetically
|
||||
slices.Sort(remaining)
|
||||
|
||||
for _, name := range remaining {
|
||||
if err := writeEntry(name, op.props[name]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteByte('}')
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (s *Schema) UnmarshalJSON(data []byte) error {
|
||||
// A JSON boolean is a valid schema.
|
||||
var b bool
|
||||
if err := json.Unmarshal(data, &b); err == nil {
|
||||
if b {
|
||||
// true is the empty schema, which validates everything.
|
||||
*s = Schema{}
|
||||
} else {
|
||||
// false is the schema that validates nothing.
|
||||
*s = *falseSchema()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
ms := struct {
|
||||
Type json.RawMessage `json:"type,omitempty"`
|
||||
Dependencies map[string]json.RawMessage `json:"dependencies,omitempty"`
|
||||
Items json.RawMessage `json:"items,omitempty"`
|
||||
Const json.RawMessage `json:"const,omitempty"`
|
||||
MinLength *integer `json:"minLength,omitempty"`
|
||||
MaxLength *integer `json:"maxLength,omitempty"`
|
||||
MinItems *integer `json:"minItems,omitempty"`
|
||||
MaxItems *integer `json:"maxItems,omitempty"`
|
||||
MinProperties *integer `json:"minProperties,omitempty"`
|
||||
MaxProperties *integer `json:"maxProperties,omitempty"`
|
||||
MinContains *integer `json:"minContains,omitempty"`
|
||||
MaxContains *integer `json:"maxContains,omitempty"`
|
||||
|
||||
*schemaWithoutMethods
|
||||
}{
|
||||
schemaWithoutMethods: (*schemaWithoutMethods)(s),
|
||||
}
|
||||
if err := unmarshalStructWithMap(data, &ms, "Extra"); err != nil {
|
||||
return err
|
||||
}
|
||||
// Unmarshal "type" as either Type or Types.
|
||||
var err error
|
||||
if len(ms.Type) > 0 {
|
||||
switch ms.Type[0] {
|
||||
case '"':
|
||||
err = json.Unmarshal(ms.Type, &s.Type)
|
||||
case '[':
|
||||
err = json.Unmarshal(ms.Type, &s.Types)
|
||||
default:
|
||||
err = fmt.Errorf(`invalid value for "type": %q`, ms.Type)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unmarshal "items" as either Items or ItemsArray.
|
||||
if len(ms.Items) > 0 {
|
||||
switch ms.Items[0] {
|
||||
case '[':
|
||||
var schemas []*Schema
|
||||
err = json.Unmarshal(ms.Items, &schemas)
|
||||
s.ItemsArray = schemas
|
||||
default:
|
||||
var schema Schema
|
||||
err = json.Unmarshal(ms.Items, &schema)
|
||||
s.Items = &schema
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unmarshal "Dependencies" values as either string arrays or schemas
|
||||
// and assign them to specific map DependencySchemas or DependencyStrings.
|
||||
for k, v := range ms.Dependencies {
|
||||
if len(v) > 0 {
|
||||
switch v[0] {
|
||||
case '[':
|
||||
var dstrings []string
|
||||
err = json.Unmarshal(v, &dstrings)
|
||||
if s.DependencyStrings == nil {
|
||||
s.DependencyStrings = make(map[string][]string)
|
||||
}
|
||||
s.DependencyStrings[k] = dstrings
|
||||
default:
|
||||
var dschema Schema
|
||||
err = json.Unmarshal(v, &dschema)
|
||||
if s.DependencySchemas == nil {
|
||||
s.DependencySchemas = make(map[string]*Schema)
|
||||
}
|
||||
s.DependencySchemas[k] = &dschema
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
unmarshalAnyPtr := func(p **any, raw json.RawMessage) error {
|
||||
if len(raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
if bytes.Equal(raw, []byte("null")) {
|
||||
*p = new(any)
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(raw, p)
|
||||
}
|
||||
|
||||
// Setting Const to a pointer to null will marshal properly, but won't
|
||||
// unmarshal: the *any is set to nil, not a pointer to nil.
|
||||
if err := unmarshalAnyPtr(&s.Const, ms.Const); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
set := func(dst **int, src *integer) {
|
||||
if src != nil {
|
||||
*dst = Ptr(int(*src))
|
||||
}
|
||||
}
|
||||
|
||||
set(&s.MinLength, ms.MinLength)
|
||||
set(&s.MaxLength, ms.MaxLength)
|
||||
set(&s.MinItems, ms.MinItems)
|
||||
set(&s.MaxItems, ms.MaxItems)
|
||||
set(&s.MinProperties, ms.MinProperties)
|
||||
set(&s.MaxProperties, ms.MaxProperties)
|
||||
set(&s.MinContains, ms.MinContains)
|
||||
set(&s.MaxContains, ms.MaxContains)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type integer int32 // for the integer-valued fields of Schema
|
||||
|
||||
func (ip *integer) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
// nothing to do
|
||||
return nil
|
||||
}
|
||||
// If there is a decimal point, src is a floating-point number.
|
||||
var i int64
|
||||
if bytes.ContainsRune(data, '.') {
|
||||
var f float64
|
||||
if err := json.Unmarshal(data, &f); err != nil {
|
||||
return errors.New("not a number")
|
||||
}
|
||||
i = int64(f)
|
||||
if float64(i) != f {
|
||||
return errors.New("not an integer value")
|
||||
}
|
||||
} else {
|
||||
if err := json.Unmarshal(data, &i); err != nil {
|
||||
return errors.New("cannot be unmarshaled into an int")
|
||||
}
|
||||
}
|
||||
// Ensure behavior is the same on both 32-bit and 64-bit systems.
|
||||
if i < math.MinInt32 || i > math.MaxInt32 {
|
||||
return errors.New("integer is out of range")
|
||||
}
|
||||
*ip = integer(i)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ptr returns a pointer to a new variable whose value is x.
|
||||
func Ptr[T any](x T) *T { return &x }
|
||||
|
||||
// every applies f preorder to every schema under s including s.
|
||||
// The second argument to f is the path to the schema appended to the argument path.
|
||||
// It stops when f returns false.
|
||||
func (s *Schema) every(f func(*Schema) bool) bool {
|
||||
return f(s) && s.everyChild(func(s *Schema) bool { return s.every(f) })
|
||||
}
|
||||
|
||||
// everyChild reports whether f is true for every immediate child schema of s.
|
||||
func (s *Schema) everyChild(f func(*Schema) bool) bool {
|
||||
v := reflect.ValueOf(s)
|
||||
for _, info := range schemaFieldInfos {
|
||||
fv := v.Elem().FieldByIndex(info.sf.Index)
|
||||
switch info.sf.Type {
|
||||
case schemaType:
|
||||
// A field that contains an individual schema. A nil is valid: it just means the field isn't present.
|
||||
c := fv.Interface().(*Schema)
|
||||
if c != nil && !f(c) {
|
||||
return false
|
||||
}
|
||||
|
||||
case schemaSliceType:
|
||||
slice := fv.Interface().([]*Schema)
|
||||
for _, c := range slice {
|
||||
if !f(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
case schemaMapType:
|
||||
// Sort keys for determinism.
|
||||
m := fv.Interface().(map[string]*Schema)
|
||||
for _, k := range slices.Sorted(maps.Keys(m)) {
|
||||
if !f(m[k]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// all wraps every in an iterator.
|
||||
func (s *Schema) all() iter.Seq[*Schema] {
|
||||
return func(yield func(*Schema) bool) { s.every(yield) }
|
||||
}
|
||||
|
||||
// children wraps everyChild in an iterator.
|
||||
func (s *Schema) children() iter.Seq[*Schema] {
|
||||
return func(yield func(*Schema) bool) { s.everyChild(yield) }
|
||||
}
|
||||
|
||||
var (
|
||||
schemaType = reflect.TypeFor[*Schema]()
|
||||
schemaSliceType = reflect.TypeFor[[]*Schema]()
|
||||
schemaMapType = reflect.TypeFor[map[string]*Schema]()
|
||||
)
|
||||
|
||||
type structFieldInfo struct {
|
||||
sf reflect.StructField
|
||||
jsonName string
|
||||
}
|
||||
|
||||
var (
|
||||
// the visible fields of Schema that have a JSON name, sorted by that name
|
||||
schemaFieldInfos []structFieldInfo
|
||||
// map from JSON name to field
|
||||
schemaFieldMap = map[string]reflect.StructField{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
t := reflect.VisibleFields(reflect.TypeFor[Schema]())
|
||||
for _, sf := range t {
|
||||
info := fieldJSONInfo(sf)
|
||||
if !info.omit {
|
||||
schemaFieldInfos = append(schemaFieldInfos, structFieldInfo{sf, info.name})
|
||||
} else {
|
||||
// jsoninfo.name is used to build the info paths. The items and dependencies are ommited,
|
||||
// since the original fields are separated to handle the union types supported in json and
|
||||
// these fields have custom marshalling and unmarshalling logic.
|
||||
// we still need these fields in schemaFieldInfos for creating schema trees and calculating paths and refs.
|
||||
// so we manually create them and assign the jsonName to the original field json name.
|
||||
switch sf.Name {
|
||||
case "Items", "ItemsArray":
|
||||
schemaFieldInfos = append(schemaFieldInfos, structFieldInfo{sf, "items"})
|
||||
case "DependencySchemas", "DependencyStrings":
|
||||
schemaFieldInfos = append(schemaFieldInfos, structFieldInfo{sf, "dependencies"})
|
||||
}
|
||||
}
|
||||
}
|
||||
// The value of "dependencies" this sort of schemaFieldInfos.
|
||||
// This sort is unstable and is comparing the json.names of DependencyStrings and DependencySchemas which are both "dependencies".
|
||||
// Since the sort is unstable it cannot be guarantied that "dependencies" has the DependencySchemas value.
|
||||
slices.SortFunc(schemaFieldInfos, func(i1, i2 structFieldInfo) int {
|
||||
return cmp.Compare(i1.jsonName, i2.jsonName)
|
||||
})
|
||||
for _, info := range schemaFieldInfos {
|
||||
schemaFieldMap[info.jsonName] = info.sf
|
||||
}
|
||||
}
|
||||
463
vendor/github.com/google/jsonschema-go/jsonschema/util.go
generated
vendored
Normal file
463
vendor/github.com/google/jsonschema-go/jsonschema/util.go
generated
vendored
Normal file
@@ -0,0 +1,463 @@
|
||||
// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash/maphash"
|
||||
"math"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Equal reports whether two Go values representing JSON values are equal according
|
||||
// to the JSON Schema spec.
|
||||
// The values must not contain cycles.
|
||||
// See https://json-schema.org/draft/2020-12/json-schema-core#section-4.2.2.
|
||||
// It behaves like reflect.DeepEqual, except that numbers are compared according
|
||||
// to mathematical equality.
|
||||
func Equal(x, y any) bool {
|
||||
return equalValue(reflect.ValueOf(x), reflect.ValueOf(y))
|
||||
}
|
||||
|
||||
func equalValue(x, y reflect.Value) bool {
|
||||
// Copied from src/reflect/deepequal.go, omitting the visited check (because JSON
|
||||
// values are trees).
|
||||
if !x.IsValid() || !y.IsValid() {
|
||||
return x.IsValid() == y.IsValid()
|
||||
}
|
||||
|
||||
// Treat numbers specially.
|
||||
rx, ok1 := jsonNumber(x)
|
||||
ry, ok2 := jsonNumber(y)
|
||||
if ok1 && ok2 {
|
||||
return rx.Cmp(ry) == 0
|
||||
}
|
||||
if x.Kind() != y.Kind() {
|
||||
return false
|
||||
}
|
||||
switch x.Kind() {
|
||||
case reflect.Array:
|
||||
if x.Len() != y.Len() {
|
||||
return false
|
||||
}
|
||||
for i := range x.Len() {
|
||||
if !equalValue(x.Index(i), y.Index(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Slice:
|
||||
if x.IsNil() != y.IsNil() {
|
||||
return false
|
||||
}
|
||||
if x.Len() != y.Len() {
|
||||
return false
|
||||
}
|
||||
if x.UnsafePointer() == y.UnsafePointer() {
|
||||
return true
|
||||
}
|
||||
// Special case for []byte, which is common.
|
||||
if x.Type().Elem().Kind() == reflect.Uint8 && x.Type() == y.Type() {
|
||||
return bytes.Equal(x.Bytes(), y.Bytes())
|
||||
}
|
||||
for i := range x.Len() {
|
||||
if !equalValue(x.Index(i), y.Index(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Interface:
|
||||
if x.IsNil() || y.IsNil() {
|
||||
return x.IsNil() == y.IsNil()
|
||||
}
|
||||
return equalValue(x.Elem(), y.Elem())
|
||||
case reflect.Pointer:
|
||||
if x.UnsafePointer() == y.UnsafePointer() {
|
||||
return true
|
||||
}
|
||||
return equalValue(x.Elem(), y.Elem())
|
||||
case reflect.Struct:
|
||||
t := x.Type()
|
||||
if t != y.Type() {
|
||||
return false
|
||||
}
|
||||
for i := range t.NumField() {
|
||||
sf := t.Field(i)
|
||||
if !sf.IsExported() {
|
||||
continue
|
||||
}
|
||||
if !equalValue(x.FieldByIndex(sf.Index), y.FieldByIndex(sf.Index)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Map:
|
||||
if x.IsNil() != y.IsNil() {
|
||||
return false
|
||||
}
|
||||
if x.Len() != y.Len() {
|
||||
return false
|
||||
}
|
||||
if x.UnsafePointer() == y.UnsafePointer() {
|
||||
return true
|
||||
}
|
||||
iter := x.MapRange()
|
||||
for iter.Next() {
|
||||
vx := iter.Value()
|
||||
vy := y.MapIndex(iter.Key())
|
||||
if !vy.IsValid() || !equalValue(vx, vy) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Func:
|
||||
if x.Type() != y.Type() {
|
||||
return false
|
||||
}
|
||||
if x.IsNil() && y.IsNil() {
|
||||
return true
|
||||
}
|
||||
panic("cannot compare functions")
|
||||
case reflect.String:
|
||||
return x.String() == y.String()
|
||||
case reflect.Bool:
|
||||
return x.Bool() == y.Bool()
|
||||
// Ints, uints and floats handled in jsonNumber, at top of function.
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported kind: %s", x.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
// hashValue adds v to the data hashed by h. v must not have cycles.
|
||||
// hashValue panics if the value contains functions or channels, or maps whose
|
||||
// key type is not string.
|
||||
// It ignores unexported fields of structs.
|
||||
// Calls to hashValue with the equal values (in the sense
|
||||
// of [Equal]) result in the same sequence of values written to the hash.
|
||||
func hashValue(h *maphash.Hash, v reflect.Value) {
|
||||
// TODO: replace writes of basic types with WriteComparable in 1.24.
|
||||
|
||||
writeUint := func(u uint64) {
|
||||
var buf [8]byte
|
||||
binary.BigEndian.PutUint64(buf[:], u)
|
||||
h.Write(buf[:])
|
||||
}
|
||||
|
||||
var write func(reflect.Value)
|
||||
write = func(v reflect.Value) {
|
||||
if r, ok := jsonNumber(v); ok {
|
||||
// We want 1.0 and 1 to hash the same.
|
||||
// big.Rats are always normalized, so they will be.
|
||||
// We could do this more efficiently by handling the int and float cases
|
||||
// separately, but that's premature.
|
||||
writeUint(uint64(r.Sign() + 1))
|
||||
h.Write(r.Num().Bytes())
|
||||
h.Write(r.Denom().Bytes())
|
||||
return
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Invalid:
|
||||
h.WriteByte(0)
|
||||
case reflect.String:
|
||||
h.WriteString(v.String())
|
||||
case reflect.Bool:
|
||||
if v.Bool() {
|
||||
h.WriteByte(1)
|
||||
} else {
|
||||
h.WriteByte(0)
|
||||
}
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
c := v.Complex()
|
||||
writeUint(math.Float64bits(real(c)))
|
||||
writeUint(math.Float64bits(imag(c)))
|
||||
case reflect.Array, reflect.Slice:
|
||||
// Although we could treat []byte more efficiently,
|
||||
// JSON values are unlikely to contain them.
|
||||
writeUint(uint64(v.Len()))
|
||||
for i := range v.Len() {
|
||||
write(v.Index(i))
|
||||
}
|
||||
case reflect.Interface, reflect.Pointer:
|
||||
write(v.Elem())
|
||||
case reflect.Struct:
|
||||
t := v.Type()
|
||||
for i := range t.NumField() {
|
||||
if sf := t.Field(i); sf.IsExported() {
|
||||
write(v.FieldByIndex(sf.Index))
|
||||
}
|
||||
}
|
||||
case reflect.Map:
|
||||
if v.Type().Key().Kind() != reflect.String {
|
||||
panic("map with non-string key")
|
||||
}
|
||||
// Sort the keys so the hash is deterministic.
|
||||
keys := v.MapKeys()
|
||||
// Write the length. That distinguishes between, say, two consecutive
|
||||
// maps with disjoint keys from one map that has the items of both.
|
||||
writeUint(uint64(len(keys)))
|
||||
slices.SortFunc(keys, func(x, y reflect.Value) int { return cmp.Compare(x.String(), y.String()) })
|
||||
for _, k := range keys {
|
||||
write(k)
|
||||
write(v.MapIndex(k))
|
||||
}
|
||||
// Ints, uints and floats handled in jsonNumber, at top of function.
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported kind: %s", v.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
write(v)
|
||||
}
|
||||
|
||||
// jsonNumber converts a numeric value or a json.Number to a [big.Rat].
|
||||
// If v is not a number, it returns nil, false.
|
||||
func jsonNumber(v reflect.Value) (*big.Rat, bool) {
|
||||
r := new(big.Rat)
|
||||
switch {
|
||||
case !v.IsValid():
|
||||
return nil, false
|
||||
case v.CanInt():
|
||||
r.SetInt64(v.Int())
|
||||
case v.CanUint():
|
||||
r.SetUint64(v.Uint())
|
||||
case v.CanFloat():
|
||||
r.SetFloat64(v.Float())
|
||||
default:
|
||||
jn, ok := v.Interface().(json.Number)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if _, ok := r.SetString(jn.String()); !ok {
|
||||
// This can fail in rare cases; for example, "1e9999999".
|
||||
// That is a valid JSON number, since the spec puts no limit on the size
|
||||
// of the exponent.
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return r, true
|
||||
}
|
||||
|
||||
// jsonType returns a string describing the type of the JSON value,
|
||||
// as described in the JSON Schema specification:
|
||||
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.1.
|
||||
// It returns "", false if the value is not valid JSON.
|
||||
func jsonType(v reflect.Value) (string, bool) {
|
||||
if !v.IsValid() {
|
||||
// Not v.IsNil(): a nil []any is still a JSON array.
|
||||
return "null", true
|
||||
}
|
||||
if v.CanInt() || v.CanUint() {
|
||||
return "integer", true
|
||||
}
|
||||
if v.CanFloat() {
|
||||
if _, f := math.Modf(v.Float()); f == 0 {
|
||||
return "integer", true
|
||||
}
|
||||
return "number", true
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Bool:
|
||||
return "boolean", true
|
||||
case reflect.String:
|
||||
return "string", true
|
||||
case reflect.Slice, reflect.Array:
|
||||
return "array", true
|
||||
case reflect.Map, reflect.Struct:
|
||||
return "object", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func assert(cond bool, msg string) {
|
||||
if !cond {
|
||||
panic("assertion failed: " + msg)
|
||||
}
|
||||
}
|
||||
|
||||
// marshalStructWithMap marshals its first argument to JSON, treating the field named
|
||||
// mapField as an embedded map. The first argument must be a pointer to
|
||||
// a struct. The underlying type of mapField must be a map[string]any, and it must have
|
||||
// a "-" json tag, meaning it will not be marshaled.
|
||||
//
|
||||
// For example, given this struct:
|
||||
//
|
||||
// type S struct {
|
||||
// A int
|
||||
// Extra map[string] any `json:"-"`
|
||||
// }
|
||||
//
|
||||
// and this value:
|
||||
//
|
||||
// s := S{A: 1, Extra: map[string]any{"B": 2}}
|
||||
//
|
||||
// the call marshalJSONWithMap(s, "Extra") would return
|
||||
//
|
||||
// {"A": 1, "B": 2}
|
||||
//
|
||||
// It is an error if the map contains the same key as another struct field's
|
||||
// JSON name.
|
||||
//
|
||||
// marshalStructWithMap calls json.Marshal on a value of type T, so T must not
|
||||
// have a MarshalJSON method that calls this function, on pain of infinite regress.
|
||||
//
|
||||
// Note that there is a similar function in mcp/util.go, but they are not the same.
|
||||
// Here the function requires `-` json tag, does not clear the mapField map,
|
||||
// and handles embedded struct due to the implementation of jsonNames in this package.
|
||||
//
|
||||
// TODO: avoid this restriction on T by forcing it to marshal in a default way.
|
||||
// See https://go.dev/play/p/EgXKJHxEx_R.
|
||||
func marshalStructWithMap[T any](s *T, mapField string) ([]byte, error) {
|
||||
// Marshal the struct and the map separately, and concatenate the bytes.
|
||||
// This strategy is dramatically less complicated than
|
||||
// constructing a synthetic struct or map with the combined keys.
|
||||
if s == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
s2 := *s
|
||||
vMapField := reflect.ValueOf(&s2).Elem().FieldByName(mapField)
|
||||
mapVal := vMapField.Interface().(map[string]any)
|
||||
|
||||
// Check for duplicates.
|
||||
names := jsonNames(reflect.TypeFor[T]())
|
||||
for key := range mapVal {
|
||||
if names[key] {
|
||||
return nil, fmt.Errorf("map key %q duplicates struct field", key)
|
||||
}
|
||||
}
|
||||
|
||||
structBytes, err := json.Marshal(s2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshalStructWithMap(%+v): %w", s, err)
|
||||
}
|
||||
if len(mapVal) == 0 {
|
||||
return structBytes, nil
|
||||
}
|
||||
mapBytes, err := json.Marshal(mapVal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(structBytes) == 2 { // must be "{}"
|
||||
return mapBytes, nil
|
||||
}
|
||||
// "{X}" + "{Y}" => "{X,Y}"
|
||||
res := append(structBytes[:len(structBytes)-1], ',')
|
||||
res = append(res, mapBytes[1:]...)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// unmarshalStructWithMap is the inverse of marshalStructWithMap.
|
||||
// T has the same restrictions as in that function.
|
||||
//
|
||||
// Note that there is a similar function in mcp/util.go, but they are not the same.
|
||||
// Here jsonNames also returns fields from embedded structs, hence this function
|
||||
// handles embedded structs as well.
|
||||
func unmarshalStructWithMap[T any](data []byte, v *T, mapField string) error {
|
||||
// Unmarshal into the struct, ignoring unknown fields.
|
||||
if err := json.Unmarshal(data, v); err != nil {
|
||||
return err
|
||||
}
|
||||
// Unmarshal into the map.
|
||||
m := map[string]any{}
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return err
|
||||
}
|
||||
// Delete from the map the fields of the struct.
|
||||
for n := range jsonNames(reflect.TypeFor[T]()) {
|
||||
delete(m, n)
|
||||
}
|
||||
if len(m) != 0 {
|
||||
reflect.ValueOf(v).Elem().FieldByName(mapField).Set(reflect.ValueOf(m))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var jsonNamesMap sync.Map // from reflect.Type to map[string]bool
|
||||
|
||||
// jsonNames returns the set of JSON object keys that t will marshal into,
|
||||
// including fields from embedded structs in t.
|
||||
// t must be a struct type.
|
||||
//
|
||||
// Note that there is a similar function in mcp/util.go, but they are not the same
|
||||
// Here the function recurses over embedded structs and includes fields from them.
|
||||
func jsonNames(t reflect.Type) map[string]bool {
|
||||
// Lock not necessary: at worst we'll duplicate work.
|
||||
if val, ok := jsonNamesMap.Load(t); ok {
|
||||
return val.(map[string]bool)
|
||||
}
|
||||
m := map[string]bool{}
|
||||
for i := range t.NumField() {
|
||||
field := t.Field(i)
|
||||
// handle embedded structs
|
||||
if field.Anonymous {
|
||||
fieldType := field.Type
|
||||
if fieldType.Kind() == reflect.Ptr {
|
||||
fieldType = fieldType.Elem()
|
||||
}
|
||||
for n := range jsonNames(fieldType) {
|
||||
m[n] = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
info := fieldJSONInfo(field)
|
||||
if !info.omit {
|
||||
m[info.name] = true
|
||||
}
|
||||
}
|
||||
jsonNamesMap.Store(t, m)
|
||||
return m
|
||||
}
|
||||
|
||||
type jsonInfo struct {
|
||||
omit bool // unexported or first tag element is "-"
|
||||
name string // Go field name or first tag element. Empty if omit is true.
|
||||
settings map[string]bool // "omitempty", "omitzero", etc.
|
||||
}
|
||||
|
||||
// fieldJSONInfo reports information about how encoding/json
|
||||
// handles the given struct field.
|
||||
// If the field is unexported, jsonInfo.omit is true and no other jsonInfo field
|
||||
// is populated.
|
||||
// If the field is exported and has no tag, then name is the field's name and all
|
||||
// other fields are false.
|
||||
// Otherwise, the information is obtained from the tag.
|
||||
func fieldJSONInfo(f reflect.StructField) jsonInfo {
|
||||
if !f.IsExported() {
|
||||
return jsonInfo{omit: true}
|
||||
}
|
||||
info := jsonInfo{name: f.Name}
|
||||
if tag, ok := f.Tag.Lookup("json"); ok {
|
||||
name, rest, found := strings.Cut(tag, ",")
|
||||
// "-" means omit, but "-," means the name is "-"
|
||||
if name == "-" && !found {
|
||||
return jsonInfo{omit: true}
|
||||
}
|
||||
if name != "" {
|
||||
info.name = name
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
info.settings = map[string]bool{}
|
||||
for _, s := range strings.Split(rest, ",") {
|
||||
info.settings[s] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
// wrapf wraps *errp with the given formatted message if *errp is not nil.
|
||||
func wrapf(errp *error, format string, args ...any) {
|
||||
if *errp != nil {
|
||||
*errp = fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), *errp)
|
||||
}
|
||||
}
|
||||
905
vendor/github.com/google/jsonschema-go/jsonschema/validate.go
generated
vendored
Normal file
905
vendor/github.com/google/jsonschema-go/jsonschema/validate.go
generated
vendored
Normal file
@@ -0,0 +1,905 @@
|
||||
// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/maphash"
|
||||
"iter"
|
||||
"math"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// The values of the "$schema" keyword for the versions that we can validate.
|
||||
const (
|
||||
draft7SchemaVersion = "http://json-schema.org/draft-07/schema#"
|
||||
draft7SecSchemaVersion = "https://json-schema.org/draft-07/schema#"
|
||||
draft202012SchemaVersion = "https://json-schema.org/draft/2020-12/schema"
|
||||
)
|
||||
|
||||
// isValidSchemaVersion checks if the given schema version is supported
|
||||
func isValidSchemaVersion(version string) bool {
|
||||
return version == "" || version == draft7SchemaVersion || version == draft7SecSchemaVersion || version == draft202012SchemaVersion
|
||||
}
|
||||
|
||||
// Validate validates the instance, which must be a JSON value, against the schema.
|
||||
// It returns nil if validation is successful or an error if it is not.
|
||||
// If the schema type is "object", instance should be a map[string]any.
|
||||
func (rs *Resolved) Validate(instance any) error {
|
||||
if s := rs.root.Schema; !isValidSchemaVersion(s) {
|
||||
return fmt.Errorf("cannot validate version %s, supported versions: draft-07 and draft 2020-12", s)
|
||||
}
|
||||
st := &state{rs: rs}
|
||||
return st.validate(reflect.ValueOf(instance), st.rs.root, nil)
|
||||
}
|
||||
|
||||
// validateDefaults walks the schema tree. If it finds a default, it validates it
|
||||
// against the schema containing it.
|
||||
//
|
||||
// TODO(jba): account for dynamic refs. This algorithm simple-mindedly
|
||||
// treats each schema with a default as its own root.
|
||||
func (rs *Resolved) validateDefaults() error {
|
||||
if s := rs.root.Schema; !isValidSchemaVersion(s) {
|
||||
return fmt.Errorf("cannot validate version %s, supported versions: draft-07 and draft 2020-12", s)
|
||||
}
|
||||
st := &state{rs: rs}
|
||||
for s := range rs.root.all() {
|
||||
// We checked for nil schemas in [Schema.Resolve].
|
||||
assert(s != nil, "nil schema")
|
||||
if s.DynamicRef != "" {
|
||||
return fmt.Errorf("jsonschema: %s: validateDefaults does not support dynamic refs", rs.schemaString(s))
|
||||
}
|
||||
if s.Default != nil {
|
||||
var d any
|
||||
if err := json.Unmarshal(s.Default, &d); err != nil {
|
||||
return fmt.Errorf("unmarshaling default value of schema %s: %w", rs.schemaString(s), err)
|
||||
}
|
||||
if err := st.validate(reflect.ValueOf(d), s, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// state is the state of single call to ResolvedSchema.Validate.
|
||||
type state struct {
|
||||
rs *Resolved
|
||||
// stack holds the schemas from recursive calls to validate.
|
||||
// These are the "dynamic scopes" used to resolve dynamic references.
|
||||
// https://json-schema.org/draft/2020-12/json-schema-core#scopes
|
||||
stack []*Schema
|
||||
}
|
||||
|
||||
// validate validates the reflected value of the instance.
|
||||
func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *annotations) (err error) {
|
||||
defer wrapf(&err, "validating %s", st.rs.schemaString(schema))
|
||||
|
||||
// Maintain a stack for dynamic schema resolution.
|
||||
st.stack = append(st.stack, schema) // push
|
||||
defer func() {
|
||||
st.stack = st.stack[:len(st.stack)-1] // pop
|
||||
}()
|
||||
|
||||
// We checked for nil schemas in [Schema.Resolve].
|
||||
assert(schema != nil, "nil schema")
|
||||
|
||||
// Step through interfaces and pointers.
|
||||
for instance.Kind() == reflect.Pointer || instance.Kind() == reflect.Interface {
|
||||
instance = instance.Elem()
|
||||
}
|
||||
|
||||
schemaInfo := st.rs.resolvedInfos[schema]
|
||||
|
||||
var anns annotations // all the annotations for this call and child calls
|
||||
// $ref: https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.1
|
||||
if schema.Ref != "" {
|
||||
if err := st.validate(instance, schemaInfo.resolvedRef, &anns); err != nil {
|
||||
return err
|
||||
}
|
||||
// https://json-schema.org/draft-07/draft-handrews-json-schema-01#rfc.section.8.3
|
||||
// "All other properties in a "$ref" object MUST be ignored."
|
||||
if st.rs.draft == draft7 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// type: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.1
|
||||
if schema.Type != "" || schema.Types != nil {
|
||||
gotType, ok := jsonType(instance)
|
||||
if !ok {
|
||||
return fmt.Errorf("type: %v of type %[1]T is not a valid JSON value", instance)
|
||||
}
|
||||
if schema.Type != "" {
|
||||
// "number" subsumes integers
|
||||
if !(gotType == schema.Type ||
|
||||
gotType == "integer" && schema.Type == "number") {
|
||||
return fmt.Errorf("type: %v has type %q, want %q", instance, gotType, schema.Type)
|
||||
}
|
||||
} else {
|
||||
if !(slices.Contains(schema.Types, gotType) || (gotType == "integer" && slices.Contains(schema.Types, "number"))) {
|
||||
return fmt.Errorf("type: %v has type %q, want one of %q",
|
||||
instance, gotType, strings.Join(schema.Types, ", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
// enum: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.2
|
||||
if schema.Enum != nil {
|
||||
ok := false
|
||||
for _, e := range schema.Enum {
|
||||
if equalValue(reflect.ValueOf(e), instance) {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("enum: %v does not equal any of: %v", instance, schema.Enum)
|
||||
}
|
||||
}
|
||||
|
||||
// const: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.3
|
||||
if schema.Const != nil {
|
||||
if !equalValue(reflect.ValueOf(*schema.Const), instance) {
|
||||
return fmt.Errorf("const: %v does not equal %v", instance, *schema.Const)
|
||||
}
|
||||
}
|
||||
|
||||
// numbers: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.2
|
||||
if schema.MultipleOf != nil || schema.Minimum != nil || schema.Maximum != nil || schema.ExclusiveMinimum != nil || schema.ExclusiveMaximum != nil {
|
||||
n, ok := jsonNumber(instance)
|
||||
if ok { // these keywords don't apply to non-numbers
|
||||
if schema.MultipleOf != nil {
|
||||
// TODO: validate MultipleOf as non-zero.
|
||||
// The test suite assumes floats.
|
||||
nf, _ := n.Float64() // don't care if it's exact or not
|
||||
if _, f := math.Modf(nf / *schema.MultipleOf); f != 0 {
|
||||
return fmt.Errorf("multipleOf: %s is not a multiple of %f", n, *schema.MultipleOf)
|
||||
}
|
||||
}
|
||||
|
||||
m := new(big.Rat) // reuse for all of the following
|
||||
cmp := func(f float64) int { return n.Cmp(m.SetFloat64(f)) }
|
||||
|
||||
if schema.Minimum != nil && cmp(*schema.Minimum) < 0 {
|
||||
return fmt.Errorf("minimum: %s is less than %f", n, *schema.Minimum)
|
||||
}
|
||||
if schema.Maximum != nil && cmp(*schema.Maximum) > 0 {
|
||||
return fmt.Errorf("maximum: %s is greater than %f", n, *schema.Maximum)
|
||||
}
|
||||
if schema.ExclusiveMinimum != nil && cmp(*schema.ExclusiveMinimum) <= 0 {
|
||||
return fmt.Errorf("exclusiveMinimum: %s is less than or equal to %f", n, *schema.ExclusiveMinimum)
|
||||
}
|
||||
if schema.ExclusiveMaximum != nil && cmp(*schema.ExclusiveMaximum) >= 0 {
|
||||
return fmt.Errorf("exclusiveMaximum: %s is greater than or equal to %f", n, *schema.ExclusiveMaximum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// strings: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.3
|
||||
if instance.Kind() == reflect.String && (schema.MinLength != nil || schema.MaxLength != nil || schema.Pattern != "") {
|
||||
str := instance.String()
|
||||
n := utf8.RuneCountInString(str)
|
||||
if schema.MinLength != nil {
|
||||
if m := *schema.MinLength; n < m {
|
||||
return fmt.Errorf("minLength: %q contains %d Unicode code points, fewer than %d", str, n, m)
|
||||
}
|
||||
}
|
||||
if schema.MaxLength != nil {
|
||||
if m := *schema.MaxLength; n > m {
|
||||
return fmt.Errorf("maxLength: %q contains %d Unicode code points, more than %d", str, n, m)
|
||||
}
|
||||
}
|
||||
|
||||
if schema.Pattern != "" && !schemaInfo.pattern.MatchString(str) {
|
||||
return fmt.Errorf("pattern: %q does not match regular expression %q", str, schema.Pattern)
|
||||
}
|
||||
}
|
||||
|
||||
// $dynamicRef: https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.2
|
||||
if schema.DynamicRef != "" {
|
||||
// The ref behaves lexically or dynamically, but not both.
|
||||
assert((schemaInfo.resolvedDynamicRef == nil) != (schemaInfo.dynamicRefAnchor == ""),
|
||||
"DynamicRef not resolved properly")
|
||||
if schemaInfo.resolvedDynamicRef != nil {
|
||||
// Same as $ref.
|
||||
if err := st.validate(instance, schemaInfo.resolvedDynamicRef, &anns); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Dynamic behavior.
|
||||
// Look for the base of the outermost schema on the stack with this dynamic
|
||||
// anchor. (Yes, outermost: the one farthest from here. This the opposite
|
||||
// of how ordinary dynamic variables behave.)
|
||||
// Why the base of the schema being validated and not the schema itself?
|
||||
// Because the base is the scope for anchors. In fact it's possible to
|
||||
// refer to a schema that is not on the stack, but a child of some base
|
||||
// on the stack.
|
||||
// For an example, search for "detached" in testdata/draft2020-12/dynamicRef.json.
|
||||
var dynamicSchema *Schema
|
||||
for _, s := range st.stack {
|
||||
base := st.rs.resolvedInfos[s].base
|
||||
info, ok := st.rs.resolvedInfos[base].anchors[schemaInfo.dynamicRefAnchor]
|
||||
if ok && info.dynamic {
|
||||
dynamicSchema = info.schema
|
||||
break
|
||||
}
|
||||
}
|
||||
if dynamicSchema == nil {
|
||||
return fmt.Errorf("missing dynamic anchor %q", schemaInfo.dynamicRefAnchor)
|
||||
}
|
||||
if err := st.validate(instance, dynamicSchema, &anns); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// logic
|
||||
// https://json-schema.org/draft/2020-12/json-schema-core#section-10.2
|
||||
// These must happen before arrays and objects because if they evaluate an item or property,
|
||||
// then the unevaluatedItems/Properties schemas don't apply to it.
|
||||
// See https://json-schema.org/draft/2020-12/json-schema-core#section-11.2, paragraph 4.
|
||||
//
|
||||
// If any of these fail, then validation fails, even if there is an unevaluatedXXX
|
||||
// keyword in the schema. The spec is unclear about this, but that is the intention.
|
||||
|
||||
valid := func(s *Schema, anns *annotations) bool { return st.validate(instance, s, anns) == nil }
|
||||
|
||||
if schema.AllOf != nil {
|
||||
for _, ss := range schema.AllOf {
|
||||
if err := st.validate(instance, ss, &anns); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if schema.AnyOf != nil {
|
||||
// We must visit them all, to collect annotations.
|
||||
ok := false
|
||||
for _, ss := range schema.AnyOf {
|
||||
if valid(ss, &anns) {
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("anyOf: did not validate against any of %v", schema.AnyOf)
|
||||
}
|
||||
}
|
||||
if schema.OneOf != nil {
|
||||
// Exactly one.
|
||||
var okSchema *Schema
|
||||
for _, ss := range schema.OneOf {
|
||||
if valid(ss, &anns) {
|
||||
if okSchema != nil {
|
||||
return fmt.Errorf("oneOf: validated against both %v and %v", okSchema, ss)
|
||||
}
|
||||
okSchema = ss
|
||||
}
|
||||
}
|
||||
if okSchema == nil {
|
||||
return fmt.Errorf("oneOf: did not validate against any of %v", schema.OneOf)
|
||||
}
|
||||
}
|
||||
if schema.Not != nil {
|
||||
// Ignore annotations from "not".
|
||||
if valid(schema.Not, nil) {
|
||||
return fmt.Errorf("not: validated against %v", schema.Not)
|
||||
}
|
||||
}
|
||||
if schema.If != nil {
|
||||
var ss *Schema
|
||||
if valid(schema.If, &anns) {
|
||||
ss = schema.Then
|
||||
} else {
|
||||
ss = schema.Else
|
||||
}
|
||||
if ss != nil {
|
||||
if err := st.validate(instance, ss, &anns); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// arrays
|
||||
if instance.Kind() == reflect.Array || instance.Kind() == reflect.Slice {
|
||||
// Handle both draft-07 and draft 2020-12
|
||||
// https://json-schema.org/draft/2020-12/json-schema-core#section-10.3.1
|
||||
// This validate call doesn't collect annotations for the items of the instance; they are separate
|
||||
// instances in their own right.
|
||||
// TODO(jba): if the test suite doesn't cover this case, add a test. For example, nested arrays.
|
||||
if st.rs.draft == draft7 {
|
||||
// For draft-07: additionalItems applies to remaining items after items array.
|
||||
// If items is a Schema or if items is not set, additionalItems should be ignored
|
||||
if schema.ItemsArray != nil {
|
||||
for i, ischema := range schema.ItemsArray {
|
||||
if i >= instance.Len() {
|
||||
break // shorter is OK
|
||||
}
|
||||
if err := st.validate(instance.Index(i), ischema, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
anns.noteEndIndex(min(len(schema.ItemsArray), instance.Len()))
|
||||
if schema.AdditionalItems != nil {
|
||||
for i := len(schema.ItemsArray); i < instance.Len(); i++ {
|
||||
if err := st.validate(instance.Index(i), schema.AdditionalItems, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
anns.allItems = true
|
||||
}
|
||||
} else if schema.Items != nil {
|
||||
for i := 0; i < instance.Len(); i++ {
|
||||
if err := st.validate(instance.Index(i), schema.Items, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Note that all the items in this array have been validated.
|
||||
anns.allItems = true
|
||||
}
|
||||
} else if st.rs.draft == draft2020 {
|
||||
// For draft 2020-12: items applies to remaining items after prefixItems
|
||||
for i, ischema := range schema.PrefixItems {
|
||||
if i >= instance.Len() {
|
||||
break // shorter is OK
|
||||
}
|
||||
if err := st.validate(instance.Index(i), ischema, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
anns.noteEndIndex(min(len(schema.PrefixItems), instance.Len()))
|
||||
if schema.Items != nil {
|
||||
for i := len(schema.PrefixItems); i < instance.Len(); i++ {
|
||||
if err := st.validate(instance.Index(i), schema.Items, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Note that all the items in this array have been validated.
|
||||
anns.allItems = true
|
||||
}
|
||||
}
|
||||
nContains := 0
|
||||
if schema.Contains != nil {
|
||||
for i := range instance.Len() {
|
||||
if err := st.validate(instance.Index(i), schema.Contains, nil); err == nil {
|
||||
nContains++
|
||||
anns.noteIndex(i)
|
||||
}
|
||||
}
|
||||
if nContains == 0 && (schema.MinContains == nil || *schema.MinContains > 0) {
|
||||
return fmt.Errorf("contains: %s does not have an item matching %s", instance, schema.Contains)
|
||||
}
|
||||
}
|
||||
|
||||
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.4
|
||||
// TODO(jba): check that these next four keywords' values are integers.
|
||||
if schema.MinContains != nil && schema.Contains != nil {
|
||||
if m := *schema.MinContains; nContains < m {
|
||||
return fmt.Errorf("minContains: contains validated %d items, less than %d", nContains, m)
|
||||
}
|
||||
}
|
||||
if schema.MaxContains != nil && schema.Contains != nil {
|
||||
if m := *schema.MaxContains; nContains > m {
|
||||
return fmt.Errorf("maxContains: contains validated %d items, greater than %d", nContains, m)
|
||||
}
|
||||
}
|
||||
if schema.MinItems != nil {
|
||||
if m := *schema.MinItems; instance.Len() < m {
|
||||
return fmt.Errorf("minItems: array length %d is less than %d", instance.Len(), m)
|
||||
}
|
||||
}
|
||||
if schema.MaxItems != nil {
|
||||
if m := *schema.MaxItems; instance.Len() > m {
|
||||
return fmt.Errorf("maxItems: array length %d is greater than %d", instance.Len(), m)
|
||||
}
|
||||
}
|
||||
if schema.UniqueItems {
|
||||
if instance.Len() > 1 {
|
||||
// Hash each item and compare the hashes.
|
||||
// If two hashes differ, the items differ.
|
||||
// If two hashes are the same, compare the collisions for equality.
|
||||
// (The same logic as hash table lookup.)
|
||||
// TODO(jba): Use container/hash.Map when it becomes available (https://go.dev/issue/69559),
|
||||
hashes := map[uint64][]int{} // from hash to indices
|
||||
seed := maphash.MakeSeed()
|
||||
for i := range instance.Len() {
|
||||
item := instance.Index(i)
|
||||
var h maphash.Hash
|
||||
h.SetSeed(seed)
|
||||
hashValue(&h, item)
|
||||
hv := h.Sum64()
|
||||
if sames := hashes[hv]; len(sames) > 0 {
|
||||
for _, j := range sames {
|
||||
if equalValue(item, instance.Index(j)) {
|
||||
return fmt.Errorf("uniqueItems: array items %d and %d are equal", i, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
hashes[hv] = append(hashes[hv], i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://json-schema.org/draft/2020-12/json-schema-core#section-11.2
|
||||
if schema.UnevaluatedItems != nil && !anns.allItems {
|
||||
// Apply this subschema to all items in the array that haven't been successfully validated.
|
||||
// That includes validations by subschemas on the same instance, like allOf.
|
||||
for i := anns.endIndex; i < instance.Len(); i++ {
|
||||
if !anns.evaluatedIndexes[i] {
|
||||
if err := st.validate(instance.Index(i), schema.UnevaluatedItems, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
anns.allItems = true
|
||||
}
|
||||
}
|
||||
|
||||
// objects
|
||||
// https://json-schema.org/draft/2020-12/json-schema-core#section-10.3.2
|
||||
// Validating structs is problematic. See https://github.com/google/jsonschema-go/issues/23.
|
||||
if instance.Kind() == reflect.Struct {
|
||||
return errors.New("cannot validate against a struct; see https://github.com/google/jsonschema-go/issues/23 for details")
|
||||
}
|
||||
if instance.Kind() == reflect.Map {
|
||||
if kt := instance.Type().Key(); kt.Kind() != reflect.String {
|
||||
return fmt.Errorf("map key type %s is not a string", kt)
|
||||
}
|
||||
// Track the evaluated properties for just this schema, to support additionalProperties.
|
||||
// If we used anns here, then we'd be including properties evaluated in subschemas
|
||||
// from allOf, etc., which additionalProperties shouldn't observe.
|
||||
evalProps := map[string]bool{}
|
||||
for prop, subschema := range schema.Properties {
|
||||
val := property(instance, prop)
|
||||
if !val.IsValid() {
|
||||
// It's OK if the instance doesn't have the property.
|
||||
continue
|
||||
}
|
||||
// If the instance is a struct and an optional property has the zero
|
||||
// value, then we could interpret it as present or missing. Be generous:
|
||||
// assume it's missing, and thus always validates successfully.
|
||||
if instance.Kind() == reflect.Struct && val.IsZero() && !schemaInfo.isRequired[prop] {
|
||||
continue
|
||||
}
|
||||
if err := st.validate(val, subschema, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
evalProps[prop] = true
|
||||
}
|
||||
if len(schema.PatternProperties) > 0 {
|
||||
for prop, val := range properties(instance) {
|
||||
// Check every matching pattern.
|
||||
for re, schema := range schemaInfo.patternProperties {
|
||||
if re.MatchString(prop) {
|
||||
if err := st.validate(val, schema, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
evalProps[prop] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if schema.AdditionalProperties != nil {
|
||||
// Special case for a better error message when additional properties is
|
||||
// 'falsy'
|
||||
//
|
||||
// If additionalProperties is {"not":{}} (which is how we
|
||||
// unmarshal "false"), we can produce a better error message that
|
||||
// summarizes all the extra properties. Otherwise, we fall back to the
|
||||
// default validation.
|
||||
//
|
||||
// Note: this is much faster than comparing with falseSchema using Equal.
|
||||
isFalsy := schema.AdditionalProperties.Not != nil && reflect.ValueOf(*schema.AdditionalProperties.Not).IsZero()
|
||||
if isFalsy {
|
||||
var disallowed []string
|
||||
for prop := range properties(instance) {
|
||||
if !evalProps[prop] {
|
||||
disallowed = append(disallowed, prop)
|
||||
}
|
||||
}
|
||||
if len(disallowed) > 0 {
|
||||
return fmt.Errorf("unexpected additional properties %q", disallowed)
|
||||
}
|
||||
} else {
|
||||
// Apply to all properties not handled above.
|
||||
for prop, val := range properties(instance) {
|
||||
if !evalProps[prop] {
|
||||
if err := st.validate(val, schema.AdditionalProperties, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
evalProps[prop] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
anns.noteProperties(evalProps)
|
||||
if schema.PropertyNames != nil {
|
||||
// Note: properties unnecessarily fetches each value. We could define a propertyNames function
|
||||
// if performance ever matters.
|
||||
for prop := range properties(instance) {
|
||||
if err := st.validate(reflect.ValueOf(prop), schema.PropertyNames, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.5
|
||||
var min, max int
|
||||
if schema.MinProperties != nil || schema.MaxProperties != nil {
|
||||
min, max = numPropertiesBounds(instance, schemaInfo.isRequired)
|
||||
}
|
||||
if schema.MinProperties != nil {
|
||||
if n, m := max, *schema.MinProperties; n < m {
|
||||
return fmt.Errorf("minProperties: object has %d properties, less than %d", n, m)
|
||||
}
|
||||
}
|
||||
if schema.MaxProperties != nil {
|
||||
if n, m := min, *schema.MaxProperties; n > m {
|
||||
return fmt.Errorf("maxProperties: object has %d properties, greater than %d", n, m)
|
||||
}
|
||||
}
|
||||
|
||||
hasProperty := func(prop string) bool {
|
||||
return property(instance, prop).IsValid()
|
||||
}
|
||||
|
||||
missingProperties := func(props []string) []string {
|
||||
var missing []string
|
||||
for _, p := range props {
|
||||
if !hasProperty(p) {
|
||||
missing = append(missing, p)
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
if schema.Required != nil {
|
||||
if m := missingProperties(schema.Required); len(m) > 0 {
|
||||
return fmt.Errorf("required: missing properties: %q", m)
|
||||
}
|
||||
}
|
||||
|
||||
if st.rs.draft == draft7 {
|
||||
if schema.DependencyStrings != nil {
|
||||
for dprop, dstrings := range schema.DependencyStrings {
|
||||
if hasProperty(dprop) {
|
||||
if m := missingProperties(dstrings); len(m) > 0 {
|
||||
return fmt.Errorf("dependentRequired[%q]: missing properties %q", dprop, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if schema.DependencySchemas != nil {
|
||||
for dprop, dschema := range schema.DependencySchemas {
|
||||
if hasProperty(dprop) {
|
||||
err := st.validate(instance, dschema, &anns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if st.rs.draft == draft2020 {
|
||||
if schema.DependentRequired != nil {
|
||||
// "Validation succeeds if, for each name that appears in both the instance
|
||||
// and as a name within this keyword's value, every item in the corresponding
|
||||
// array is also the name of a property in the instance." §6.5.4
|
||||
for dprop, reqs := range schema.DependentRequired {
|
||||
if hasProperty(dprop) {
|
||||
if m := missingProperties(reqs); len(m) > 0 {
|
||||
return fmt.Errorf("dependentRequired[%q]: missing properties %q", dprop, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://json-schema.org/draft/2020-12/json-schema-core#section-10.2.2.4
|
||||
if schema.DependentSchemas != nil {
|
||||
// This does not collect annotations, although it seems like it should.
|
||||
for dprop, ss := range schema.DependentSchemas {
|
||||
if hasProperty(dprop) {
|
||||
// TODO: include dependentSchemas[dprop] in the errors.
|
||||
err := st.validate(instance, ss, &anns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if schema.UnevaluatedProperties != nil && !anns.allProperties {
|
||||
// This looks a lot like AdditionalProperties, but depends on in-place keywords like allOf
|
||||
// in addition to sibling keywords.
|
||||
for prop, val := range properties(instance) {
|
||||
if !anns.evaluatedProperties[prop] {
|
||||
if err := st.validate(val, schema.UnevaluatedProperties, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
// The spec says the annotation should be the set of evaluated properties, but we can optimize
|
||||
// by setting a single boolean, since after this succeeds all properties will be validated.
|
||||
// See https://json-schema.slack.com/archives/CT7FF623C/p1745592564381459.
|
||||
anns.allProperties = true
|
||||
}
|
||||
}
|
||||
|
||||
if callerAnns != nil {
|
||||
// Our caller wants to know what we've validated.
|
||||
callerAnns.merge(&anns)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveDynamicRef returns the schema referred to by the argument schema's
|
||||
// $dynamicRef value.
|
||||
// It returns an error if the dynamic reference has no referent.
|
||||
// If there is no $dynamicRef, resolveDynamicRef returns nil, nil.
|
||||
// See https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.2.
|
||||
func (st *state) resolveDynamicRef(schema *Schema) (*Schema, error) {
|
||||
if schema.DynamicRef == "" {
|
||||
return nil, nil
|
||||
}
|
||||
info := st.rs.resolvedInfos[schema]
|
||||
// The ref behaves lexically or dynamically, but not both.
|
||||
assert((info.resolvedDynamicRef == nil) != (info.dynamicRefAnchor == ""),
|
||||
"DynamicRef not statically resolved properly")
|
||||
if r := info.resolvedDynamicRef; r != nil {
|
||||
// Same as $ref.
|
||||
return r, nil
|
||||
}
|
||||
// Dynamic behavior.
|
||||
// Look for the base of the outermost schema on the stack with this dynamic
|
||||
// anchor. (Yes, outermost: the one farthest from here. This the opposite
|
||||
// of how ordinary dynamic variables behave.)
|
||||
// Why the base of the schema being validated and not the schema itself?
|
||||
// Because the base is the scope for anchors. In fact it's possible to
|
||||
// refer to a schema that is not on the stack, but a child of some base
|
||||
// on the stack.
|
||||
// For an example, search for "detached" in testdata/draft2020-12/dynamicRef.json.
|
||||
for _, s := range st.stack {
|
||||
base := st.rs.resolvedInfos[s].base
|
||||
info, ok := st.rs.resolvedInfos[base].anchors[info.dynamicRefAnchor]
|
||||
if ok && info.dynamic {
|
||||
return info.schema, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("missing dynamic anchor %q", info.dynamicRefAnchor)
|
||||
}
|
||||
|
||||
// ApplyDefaults modifies an instance by applying the schema's defaults to it. If
|
||||
// a schema or sub-schema has a default, then a corresponding missing instance value
|
||||
// is set to the default.
|
||||
//
|
||||
// The JSON Schema specification does not describe how defaults should be interpreted.
|
||||
// This method honors defaults only on properties, and only those that are not required.
|
||||
// If the instance is a map and the property is missing, the property is added to
|
||||
// the map with the default.
|
||||
// ApplyDefaults does not support structs, because it cannot know whether a field
|
||||
// is missing in the JSON, or was explicitly set to its zero value.
|
||||
//
|
||||
// ApplyDefaults can panic if a default cannot be assigned to a field.
|
||||
//
|
||||
// The argument must be a pointer to the instance.
|
||||
// (In case we decide that top-level defaults are meaningful.)
|
||||
//
|
||||
// It is recommended to first call Resolve with a ValidateDefaults option of true,
|
||||
// then call this method, and lastly call Validate.
|
||||
func (rs *Resolved) ApplyDefaults(instancep any) error {
|
||||
// TODO(jba): consider what defaults on top-level or array instances might mean.
|
||||
// TODO(jba): follow $ref and $dynamicRef
|
||||
st := &state{rs: rs}
|
||||
return st.applyDefaults(reflect.ValueOf(instancep), rs.root)
|
||||
}
|
||||
|
||||
// Recursive helper used by ApplyDefaults. Applies defaults on sub-schemas
|
||||
// of object properties recursively.
|
||||
func (st *state) applyDefaults(instancep reflect.Value, schema *Schema) (err error) {
|
||||
defer wrapf(&err, "applyDefaults: schema %s, instance %v", st.rs.schemaString(schema), instancep)
|
||||
|
||||
schemaInfo := st.rs.resolvedInfos[schema]
|
||||
instance := instancep.Elem()
|
||||
if instance.Kind() == reflect.Interface && instance.IsValid() {
|
||||
// If we unmarshalled into 'any', the default object unmarshalling will be map[string]any.
|
||||
instance = instance.Elem()
|
||||
}
|
||||
if instance.Kind() == reflect.Map || instance.Kind() == reflect.Struct {
|
||||
if instance.Kind() == reflect.Map {
|
||||
if kt := instance.Type().Key(); kt.Kind() != reflect.String {
|
||||
return fmt.Errorf("map key type %s is not a string", kt)
|
||||
}
|
||||
}
|
||||
for prop, subschema := range schema.Properties {
|
||||
// Ignore defaults on required properties. (A required property shouldn't have a default.)
|
||||
if schemaInfo.isRequired[prop] {
|
||||
continue
|
||||
}
|
||||
val := property(instance, prop)
|
||||
switch instance.Kind() {
|
||||
case reflect.Map:
|
||||
// If there is a default for this property, and the map key is missing,
|
||||
// set the map value to the default.
|
||||
if subschema.Default != nil && !val.IsValid() {
|
||||
// Create an lvalue, since map values aren't addressable.
|
||||
lvalue := reflect.New(instance.Type().Elem())
|
||||
if err := json.Unmarshal(subschema.Default, lvalue.Interface()); err != nil {
|
||||
return err
|
||||
}
|
||||
// Recurse unconditionally; applyDefaults will only act on object-like values.
|
||||
if err := st.applyDefaults(lvalue, subschema); err != nil {
|
||||
return err
|
||||
}
|
||||
instance.SetMapIndex(reflect.ValueOf(prop), lvalue.Elem())
|
||||
} else if val.IsValid() {
|
||||
// Recurse into an existing sub-instance.
|
||||
// MapIndex returns a non-addressable value; copy into an addressable lvalue, recurse, then set back.
|
||||
lvalue := reflect.New(instance.Type().Elem())
|
||||
// Initialize the lvalue with current value.
|
||||
lvalue.Elem().Set(val)
|
||||
if err := st.applyDefaults(lvalue, subschema); err != nil {
|
||||
return err
|
||||
}
|
||||
instance.SetMapIndex(reflect.ValueOf(prop), lvalue.Elem())
|
||||
} else if schemaHasDefaultsInProperties(subschema) {
|
||||
// Property is missing, but descendants still have some defaults
|
||||
// Create an empty container and recurse to populate
|
||||
elemType := instance.Type().Elem()
|
||||
var child reflect.Value
|
||||
switch elemType.Kind() {
|
||||
case reflect.Interface:
|
||||
child = reflect.ValueOf(map[string]any{})
|
||||
case reflect.Map:
|
||||
child = reflect.MakeMap(elemType)
|
||||
case reflect.Struct:
|
||||
child = reflect.New(elemType).Elem()
|
||||
}
|
||||
if child.IsValid() {
|
||||
lvalue := reflect.New(elemType)
|
||||
lvalue.Elem().Set(child)
|
||||
if err := st.applyDefaults(lvalue, subschema); err != nil {
|
||||
return err
|
||||
}
|
||||
instance.SetMapIndex(reflect.ValueOf(prop), lvalue.Elem())
|
||||
}
|
||||
}
|
||||
case reflect.Struct:
|
||||
return errors.New("cannot apply defaults to a struct")
|
||||
default:
|
||||
panic(fmt.Sprintf("applyDefaults: property %s: bad value %s of kind %s",
|
||||
prop, instance, instance.Kind()))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// schemaHasDefaultsInProperties reports whether s or any descendant schema under
|
||||
// its Properties contains a default. Only walks Properties to match ApplyDefaults semantics.
|
||||
func schemaHasDefaultsInProperties(s *Schema) bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
if s.Default != nil {
|
||||
return true
|
||||
}
|
||||
if s.Properties != nil {
|
||||
for _, ss := range s.Properties {
|
||||
if schemaHasDefaultsInProperties(ss) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// property returns the value of the property of v with the given name, or the invalid
|
||||
// reflect.Value if there is none.
|
||||
// If v is a map, the property is the value of the map whose key is name.
|
||||
// If v is a struct, the property is the value of the field with the given name according
|
||||
// to the encoding/json package (see [jsonName]).
|
||||
// If v is anything else, property panics.
|
||||
func property(v reflect.Value, name string) reflect.Value {
|
||||
switch v.Kind() {
|
||||
case reflect.Map:
|
||||
return v.MapIndex(reflect.ValueOf(name))
|
||||
case reflect.Struct:
|
||||
props := structPropertiesOf(v.Type())
|
||||
// Ignore nonexistent properties.
|
||||
if sf, ok := props[name]; ok {
|
||||
return v.FieldByIndex(sf.Index)
|
||||
}
|
||||
return reflect.Value{}
|
||||
default:
|
||||
panic(fmt.Sprintf("property(%q): bad value %s of kind %s", name, v, v.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
// properties returns an iterator over the names and values of all properties
|
||||
// in v, which must be a map or a struct.
|
||||
// If a struct, zero-valued properties that are marked omitempty or omitzero
|
||||
// are excluded.
|
||||
func properties(v reflect.Value) iter.Seq2[string, reflect.Value] {
|
||||
return func(yield func(string, reflect.Value) bool) {
|
||||
switch v.Kind() {
|
||||
case reflect.Map:
|
||||
for k, e := range v.Seq2() {
|
||||
if !yield(k.String(), e) {
|
||||
return
|
||||
}
|
||||
}
|
||||
case reflect.Struct:
|
||||
for name, sf := range structPropertiesOf(v.Type()) {
|
||||
val := v.FieldByIndex(sf.Index)
|
||||
if val.IsZero() {
|
||||
info := fieldJSONInfo(sf)
|
||||
if info.settings["omitempty"] || info.settings["omitzero"] {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !yield(name, val) {
|
||||
return
|
||||
}
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("bad value %s of kind %s", v, v.Kind()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// numPropertiesBounds returns bounds on the number of v's properties.
|
||||
// v must be a map or a struct.
|
||||
// If v is a map, both bounds are the map's size.
|
||||
// If v is a struct, the max is the number of struct properties.
|
||||
// But since we don't know whether a zero value indicates a missing optional property
|
||||
// or not, be generous and use the number of non-zero properties as the min.
|
||||
func numPropertiesBounds(v reflect.Value, isRequired map[string]bool) (int, int) {
|
||||
switch v.Kind() {
|
||||
case reflect.Map:
|
||||
return v.Len(), v.Len()
|
||||
case reflect.Struct:
|
||||
sp := structPropertiesOf(v.Type())
|
||||
min := 0
|
||||
for prop, sf := range sp {
|
||||
if !v.FieldByIndex(sf.Index).IsZero() || isRequired[prop] {
|
||||
min++
|
||||
}
|
||||
}
|
||||
return min, len(sp)
|
||||
default:
|
||||
panic(fmt.Sprintf("properties: bad value: %s of kind %s", v, v.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
// A propertyMap is a map from property name to struct field index.
|
||||
type propertyMap = map[string]reflect.StructField
|
||||
|
||||
var structProperties sync.Map // from reflect.Type to propertyMap
|
||||
|
||||
// structPropertiesOf returns the JSON Schema properties for the struct type t.
|
||||
// The caller must not mutate the result.
|
||||
func structPropertiesOf(t reflect.Type) propertyMap {
|
||||
// Mutex not necessary: at worst we'll recompute the same value.
|
||||
if props, ok := structProperties.Load(t); ok {
|
||||
return props.(propertyMap)
|
||||
}
|
||||
props := map[string]reflect.StructField{}
|
||||
for _, sf := range reflect.VisibleFields(t) {
|
||||
if sf.Anonymous {
|
||||
continue
|
||||
}
|
||||
info := fieldJSONInfo(sf)
|
||||
if !info.omit {
|
||||
props[info.name] = sf
|
||||
}
|
||||
}
|
||||
structProperties.Store(t, props)
|
||||
return props
|
||||
}
|
||||
2
vendor/github.com/invopop/jsonschema/.gitignore
generated
vendored
2
vendor/github.com/invopop/jsonschema/.gitignore
generated
vendored
@@ -1,2 +0,0 @@
|
||||
vendor/
|
||||
.idea/
|
||||
69
vendor/github.com/invopop/jsonschema/.golangci.yml
generated
vendored
69
vendor/github.com/invopop/jsonschema/.golangci.yml
generated
vendored
@@ -1,69 +0,0 @@
|
||||
run:
|
||||
tests: true
|
||||
max-same-issues: 50
|
||||
|
||||
output:
|
||||
print-issued-lines: false
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- gocyclo
|
||||
- gocritic
|
||||
- goconst
|
||||
- dupl
|
||||
- unconvert
|
||||
- goimports
|
||||
- unused
|
||||
- govet
|
||||
- nakedret
|
||||
- errcheck
|
||||
- revive
|
||||
- ineffassign
|
||||
- goconst
|
||||
- unparam
|
||||
- gofmt
|
||||
|
||||
linters-settings:
|
||||
vet:
|
||||
check-shadowing: true
|
||||
use-installed-packages: true
|
||||
dupl:
|
||||
threshold: 100
|
||||
goconst:
|
||||
min-len: 8
|
||||
min-occurrences: 3
|
||||
gocyclo:
|
||||
min-complexity: 20
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- ifElseChain
|
||||
gofmt:
|
||||
rewrite-rules:
|
||||
- pattern: "interface{}"
|
||||
replacement: "any"
|
||||
- pattern: "a[b:len(a)]"
|
||||
replacement: "a[b:]"
|
||||
|
||||
issues:
|
||||
max-per-linter: 0
|
||||
max-same: 0
|
||||
exclude-dirs:
|
||||
- resources
|
||||
- old
|
||||
exclude-files:
|
||||
- cmd/protopkg/main.go
|
||||
exclude-use-default: false
|
||||
exclude:
|
||||
# Captured by errcheck.
|
||||
- "^(G104|G204):"
|
||||
# Very commonly not checked.
|
||||
- 'Error return value of .(.*\.Help|.*\.MarkFlagRequired|(os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*Print(f|ln|)|os\.(Un)?Setenv). is not checked'
|
||||
# Weird error only seen on Kochiku...
|
||||
- "internal error: no range for"
|
||||
- 'exported method `.*\.(MarshalJSON|UnmarshalJSON|URN|Payload|GoString|Close|Provides|Requires|ExcludeFromHash|MarshalText|UnmarshalText|Description|Check|Poll|Severity)` should have comment or be unexported'
|
||||
- "composite literal uses unkeyed fields"
|
||||
- 'declaration of "err" shadows declaration'
|
||||
- "by other packages, and that stutters"
|
||||
- "Potential file inclusion via variable"
|
||||
- "at least one file in a package should have a package comment"
|
||||
- "bad syntax for struct tag pair"
|
||||
19
vendor/github.com/invopop/jsonschema/COPYING
generated
vendored
19
vendor/github.com/invopop/jsonschema/COPYING
generated
vendored
@@ -1,19 +0,0 @@
|
||||
Copyright (C) 2014 Alec Thomas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
374
vendor/github.com/invopop/jsonschema/README.md
generated
vendored
374
vendor/github.com/invopop/jsonschema/README.md
generated
vendored
@@ -1,374 +0,0 @@
|
||||
# Go JSON Schema Reflection
|
||||
|
||||
[](https://github.com/invopop/jsonschema/actions/workflows/lint.yaml)
|
||||
[](https://github.com/invopop/jsonschema/actions/workflows/test.yaml)
|
||||
[](https://goreportcard.com/report/github.com/invopop/jsonschema)
|
||||
[](https://godoc.org/github.com/invopop/jsonschema)
|
||||
[](https://codecov.io/gh/invopop/jsonschema)
|
||||

|
||||
|
||||
This package can be used to generate [JSON Schemas](http://json-schema.org/latest/json-schema-validation.html) from Go types through reflection.
|
||||
|
||||
- Supports arbitrarily complex types, including `interface{}`, maps, slices, etc.
|
||||
- Supports json-schema features such as minLength, maxLength, pattern, format, etc.
|
||||
- Supports simple string and numeric enums.
|
||||
- Supports custom property fields via the `jsonschema_extras` struct tag.
|
||||
|
||||
This repository is a fork of the original [jsonschema](https://github.com/alecthomas/jsonschema) by [@alecthomas](https://github.com/alecthomas). At [Invopop](https://invopop.com) we use jsonschema as a cornerstone in our [GOBL library](https://github.com/invopop/gobl), and wanted to be able to continue building and adding features without taking up Alec's time. There have been a few significant changes that probably mean this version is a not compatible with with Alec's:
|
||||
|
||||
- The original was stuck on the draft-04 version of JSON Schema, we've now moved to the latest JSON Schema Draft 2020-12.
|
||||
- Schema IDs are added automatically from the current Go package's URL in order to be unique, and can be disabled with the `Anonymous` option.
|
||||
- Support for the `FullyQualifyTypeName` option has been removed. If you have conflicts, you should use multiple schema files with different IDs, set the `DoNotReference` option to true to hide definitions completely, or add your own naming strategy using the `Namer` property.
|
||||
- Support for `yaml` tags and related options has been dropped for the sake of simplification. There were a [few inconsistencies](https://github.com/invopop/jsonschema/pull/21) around this that have now been fixed.
|
||||
|
||||
## Versions
|
||||
|
||||
This project is still under v0 scheme, as per Go convention, breaking changes are likely. Please pin go modules to version tags or branches, and reach out if you think something can be improved.
|
||||
|
||||
Go version >= 1.18 is required as generics are now being used.
|
||||
|
||||
## Example
|
||||
|
||||
The following Go type:
|
||||
|
||||
```go
|
||||
type TestUser struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name" jsonschema:"title=the name,description=The name of a friend,example=joe,example=lucy,default=alex"`
|
||||
Friends []int `json:"friends,omitempty" jsonschema_description:"The list of IDs, omitted when empty"`
|
||||
Tags map[string]interface{} `json:"tags,omitempty" jsonschema_extras:"a=b,foo=bar,foo=bar1"`
|
||||
BirthDate time.Time `json:"birth_date,omitempty" jsonschema:"oneof_required=date"`
|
||||
YearOfBirth string `json:"year_of_birth,omitempty" jsonschema:"oneof_required=year"`
|
||||
Metadata interface{} `json:"metadata,omitempty" jsonschema:"oneof_type=string;array"`
|
||||
FavColor string `json:"fav_color,omitempty" jsonschema:"enum=red,enum=green,enum=blue"`
|
||||
}
|
||||
```
|
||||
|
||||
Results in following JSON Schema:
|
||||
|
||||
```go
|
||||
jsonschema.Reflect(&TestUser{})
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://github.com/invopop/jsonschema_test/test-user",
|
||||
"$ref": "#/$defs/TestUser",
|
||||
"$defs": {
|
||||
"TestUser": {
|
||||
"oneOf": [
|
||||
{
|
||||
"required": ["birth_date"],
|
||||
"title": "date"
|
||||
},
|
||||
{
|
||||
"required": ["year_of_birth"],
|
||||
"title": "year"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "the name",
|
||||
"description": "The name of a friend",
|
||||
"default": "alex",
|
||||
"examples": ["joe", "lucy"]
|
||||
},
|
||||
"friends": {
|
||||
"items": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "array",
|
||||
"description": "The list of IDs, omitted when empty"
|
||||
},
|
||||
"tags": {
|
||||
"type": "object",
|
||||
"a": "b",
|
||||
"foo": ["bar", "bar1"]
|
||||
},
|
||||
"birth_date": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"year_of_birth": {
|
||||
"type": "string"
|
||||
},
|
||||
"metadata": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
},
|
||||
"fav_color": {
|
||||
"type": "string",
|
||||
"enum": ["red", "green", "blue"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": ["id", "name"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## YAML
|
||||
|
||||
Support for `yaml` tags has now been removed. If you feel very strongly about this, we've opened a discussion to hear your comments: https://github.com/invopop/jsonschema/discussions/28
|
||||
|
||||
The recommended approach if you need to deal with YAML data is to first convert to JSON. The [invopop/yaml](https://github.com/invopop/yaml) library will make this trivial.
|
||||
|
||||
## Configurable behaviour
|
||||
|
||||
The behaviour of the schema generator can be altered with parameters when a `jsonschema.Reflector`
|
||||
instance is created.
|
||||
|
||||
### ExpandedStruct
|
||||
|
||||
If set to `true`, makes the top level struct not to reference itself in the definitions. But type passed should be a struct type.
|
||||
|
||||
eg.
|
||||
|
||||
```go
|
||||
type GrandfatherType struct {
|
||||
FamilyName string `json:"family_name" jsonschema:"required"`
|
||||
}
|
||||
|
||||
type SomeBaseType struct {
|
||||
SomeBaseProperty int `json:"some_base_property"`
|
||||
// The jsonschema required tag is nonsensical for private and ignored properties.
|
||||
// Their presence here tests that the fields *will not* be required in the output
|
||||
// schema, even if they are tagged required.
|
||||
somePrivateBaseProperty string `json:"i_am_private" jsonschema:"required"`
|
||||
SomeIgnoredBaseProperty string `json:"-" jsonschema:"required"`
|
||||
SomeSchemaIgnoredProperty string `jsonschema:"-,required"`
|
||||
SomeUntaggedBaseProperty bool `jsonschema:"required"`
|
||||
someUnexportedUntaggedBaseProperty bool
|
||||
Grandfather GrandfatherType `json:"grand"`
|
||||
}
|
||||
```
|
||||
|
||||
will output:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft/2020-12/schema",
|
||||
"required": ["some_base_property", "grand", "SomeUntaggedBaseProperty"],
|
||||
"properties": {
|
||||
"SomeUntaggedBaseProperty": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"grand": {
|
||||
"$schema": "http://json-schema.org/draft/2020-12/schema",
|
||||
"$ref": "#/definitions/GrandfatherType"
|
||||
},
|
||||
"some_base_property": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"$defs": {
|
||||
"GrandfatherType": {
|
||||
"required": ["family_name"],
|
||||
"properties": {
|
||||
"family_name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Go Comments
|
||||
|
||||
Writing a good schema with descriptions inside tags can become cumbersome and tedious, especially if you already have some Go comments around your types and field definitions. If you'd like to take advantage of these existing comments, you can use the `AddGoComments(base, path string)` method that forms part of the reflector to parse your go files and automatically generate a dictionary of Go import paths, types, and fields, to individual comments. These will then be used automatically as description fields, and can be overridden with a manual definition if needed.
|
||||
|
||||
Take a simplified example of a User struct which for the sake of simplicity we assume is defined inside this package:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
// User is used as a base to provide tests for comments.
|
||||
type User struct {
|
||||
// Unique sequential identifier.
|
||||
ID int `json:"id" jsonschema:"required"`
|
||||
// Name of the user
|
||||
Name string `json:"name"`
|
||||
}
|
||||
```
|
||||
|
||||
To get the comments provided into your JSON schema, use a regular `Reflector` and add the go code using an import module URL and path. Fully qualified go module paths cannot be determined reliably by the `go/parser` library, so we need to introduce this manually:
|
||||
|
||||
```go
|
||||
r := new(Reflector)
|
||||
if err := r.AddGoComments("github.com/invopop/jsonschema", "./"); err != nil {
|
||||
// deal with error
|
||||
}
|
||||
s := r.Reflect(&User{})
|
||||
// output
|
||||
```
|
||||
|
||||
Expect the results to be similar to:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft/2020-12/schema",
|
||||
"$ref": "#/$defs/User",
|
||||
"$defs": {
|
||||
"User": {
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"description": "Unique sequential identifier."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the user"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"description": "User is used as a base to provide tests for comments."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Key Naming
|
||||
|
||||
In some situations, the keys actually used to write files are different from Go structs'.
|
||||
|
||||
This is often the case when writing a configuration file to YAML or JSON from a Go struct, or when returning a JSON response for a Web API: APIs typically use snake_case, while Go uses PascalCase.
|
||||
|
||||
You can pass a `func(string) string` function to `Reflector`'s `KeyNamer` option to map Go field names to JSON key names and reflect the aforementioned transformations, without having to specify `json:"..."` on every struct field.
|
||||
|
||||
For example, consider the following struct
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
GivenName string
|
||||
PasswordSalted []byte `json:"salted_password"`
|
||||
}
|
||||
```
|
||||
|
||||
We can transform field names to snake_case in the generated JSON schema:
|
||||
|
||||
```go
|
||||
r := new(jsonschema.Reflector)
|
||||
r.KeyNamer = strcase.SnakeCase // from package github.com/stoewer/go-strcase
|
||||
|
||||
r.Reflect(&User{})
|
||||
```
|
||||
|
||||
Will yield
|
||||
|
||||
```diff
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft/2020-12/schema",
|
||||
"$ref": "#/$defs/User",
|
||||
"$defs": {
|
||||
"User": {
|
||||
"properties": {
|
||||
- "GivenName": {
|
||||
+ "given_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"salted_password": {
|
||||
"type": "string",
|
||||
"contentEncoding": "base64"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
- "required": ["GivenName", "salted_password"]
|
||||
+ "required": ["given_name", "salted_password"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, if a field name has a `json:""` tag set, the `key` argument to `KeyNamer` will have the value of that tag.
|
||||
|
||||
### Custom Type Definitions
|
||||
|
||||
Sometimes it can be useful to have custom JSON Marshal and Unmarshal methods in your structs that automatically convert for example a string into an object.
|
||||
|
||||
This library will recognize and attempt to call four different methods that help you adjust schemas to your specific needs:
|
||||
|
||||
- `JSONSchema() *Schema` - will prevent auto-generation of the schema so that you can provide your own definition.
|
||||
- `JSONSchemaExtend(schema *jsonschema.Schema)` - will be called _after_ the schema has been generated, allowing you to add or manipulate the fields easily.
|
||||
- `JSONSchemaAlias() any` - is called when reflecting the type of object and allows for an alternative to be used instead.
|
||||
- `JSONSchemaProperty(prop string) any` - will be called for every property inside a struct giving you the chance to provide an alternative object to convert into a schema.
|
||||
|
||||
Note that all of these methods **must** be defined on a non-pointer object for them to be called.
|
||||
|
||||
Take the following simplified example of a `CompactDate` that only includes the Year and Month:
|
||||
|
||||
```go
|
||||
type CompactDate struct {
|
||||
Year int
|
||||
Month int
|
||||
}
|
||||
|
||||
func (d *CompactDate) UnmarshalJSON(data []byte) error {
|
||||
if len(data) != 9 {
|
||||
return errors.New("invalid compact date length")
|
||||
}
|
||||
var err error
|
||||
d.Year, err = strconv.Atoi(string(data[1:5]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.Month, err = strconv.Atoi(string(data[7:8]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *CompactDate) MarshalJSON() ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
buf.WriteByte('"')
|
||||
buf.WriteString(fmt.Sprintf("%d-%02d", d.Year, d.Month))
|
||||
buf.WriteByte('"')
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (CompactDate) JSONSchema() *Schema {
|
||||
return &Schema{
|
||||
Type: "string",
|
||||
Title: "Compact Date",
|
||||
Description: "Short date that only includes year and month",
|
||||
Pattern: "^[0-9]{4}-[0-1][0-9]$",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The resulting schema generated for this struct would look like:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft/2020-12/schema",
|
||||
"$ref": "#/$defs/CompactDate",
|
||||
"$defs": {
|
||||
"CompactDate": {
|
||||
"pattern": "^[0-9]{4}-[0-1][0-9]$",
|
||||
"type": "string",
|
||||
"title": "Compact Date",
|
||||
"description": "Short date that only includes year and month"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
76
vendor/github.com/invopop/jsonschema/id.go
generated
vendored
76
vendor/github.com/invopop/jsonschema/id.go
generated
vendored
@@ -1,76 +0,0 @@
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ID represents a Schema ID type which should always be a URI.
|
||||
// See draft-bhutton-json-schema-00 section 8.2.1
|
||||
type ID string
|
||||
|
||||
// EmptyID is used to explicitly define an ID with no value.
|
||||
const EmptyID ID = ""
|
||||
|
||||
// Validate is used to check if the ID looks like a proper schema.
|
||||
// This is done by parsing the ID as a URL and checking it has all the
|
||||
// relevant parts.
|
||||
func (id ID) Validate() error {
|
||||
u, err := url.Parse(id.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid URL: %w", err)
|
||||
}
|
||||
if u.Hostname() == "" {
|
||||
return errors.New("missing hostname")
|
||||
}
|
||||
if !strings.Contains(u.Hostname(), ".") {
|
||||
return errors.New("hostname does not look valid")
|
||||
}
|
||||
if u.Path == "" {
|
||||
return errors.New("path is expected")
|
||||
}
|
||||
if u.Scheme != "https" && u.Scheme != "http" {
|
||||
return errors.New("unexpected schema")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Anchor sets the anchor part of the schema URI.
|
||||
func (id ID) Anchor(name string) ID {
|
||||
b := id.Base()
|
||||
return ID(b.String() + "#" + name)
|
||||
}
|
||||
|
||||
// Def adds or replaces a definition identifier.
|
||||
func (id ID) Def(name string) ID {
|
||||
b := id.Base()
|
||||
return ID(b.String() + "#/$defs/" + name)
|
||||
}
|
||||
|
||||
// Add appends the provided path to the id, and removes any
|
||||
// anchor data that might be there.
|
||||
func (id ID) Add(path string) ID {
|
||||
b := id.Base()
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
return ID(b.String() + path)
|
||||
}
|
||||
|
||||
// Base removes any anchor information from the schema
|
||||
func (id ID) Base() ID {
|
||||
s := id.String()
|
||||
i := strings.LastIndex(s, "#")
|
||||
if i != -1 {
|
||||
s = s[0:i]
|
||||
}
|
||||
s = strings.TrimRight(s, "/")
|
||||
return ID(s)
|
||||
}
|
||||
|
||||
// String provides string version of ID
|
||||
func (id ID) String() string {
|
||||
return string(id)
|
||||
}
|
||||
1148
vendor/github.com/invopop/jsonschema/reflect.go
generated
vendored
1148
vendor/github.com/invopop/jsonschema/reflect.go
generated
vendored
File diff suppressed because it is too large
Load Diff
146
vendor/github.com/invopop/jsonschema/reflect_comments.go
generated
vendored
146
vendor/github.com/invopop/jsonschema/reflect_comments.go
generated
vendored
@@ -1,146 +0,0 @@
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
gopath "path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"go/ast"
|
||||
"go/doc"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
)
|
||||
|
||||
type commentOptions struct {
|
||||
fullObjectText bool // use the first sentence only?
|
||||
}
|
||||
|
||||
// CommentOption allows for special configuration options when preparing Go
|
||||
// source files for comment extraction.
|
||||
type CommentOption func(*commentOptions)
|
||||
|
||||
// WithFullComment will configure the comment extraction to process to use an
|
||||
// object type's full comment text instead of just the synopsis.
|
||||
func WithFullComment() CommentOption {
|
||||
return func(o *commentOptions) {
|
||||
o.fullObjectText = true
|
||||
}
|
||||
}
|
||||
|
||||
// AddGoComments will update the reflectors comment map with all the comments
|
||||
// found in the provided source directories including sub-directories, in order to
|
||||
// generate a dictionary of comments associated with Types and Fields. The results
|
||||
// will be added to the `Reflect.CommentMap` ready to use with Schema "description"
|
||||
// fields.
|
||||
//
|
||||
// The `go/parser` library is used to extract all the comments and unfortunately doesn't
|
||||
// have a built-in way to determine the fully qualified name of a package. The `base`
|
||||
// parameter, the URL used to import that package, is thus required to be able to match
|
||||
// reflected types.
|
||||
//
|
||||
// When parsing type comments, by default we use the `go/doc`'s Synopsis method to extract
|
||||
// the first phrase only. Field comments, which tend to be much shorter, will include everything.
|
||||
// This behavior can be changed by using the `WithFullComment` option.
|
||||
func (r *Reflector) AddGoComments(base, path string, opts ...CommentOption) error {
|
||||
if r.CommentMap == nil {
|
||||
r.CommentMap = make(map[string]string)
|
||||
}
|
||||
co := new(commentOptions)
|
||||
for _, opt := range opts {
|
||||
opt(co)
|
||||
}
|
||||
|
||||
return r.extractGoComments(base, path, r.CommentMap, co)
|
||||
}
|
||||
|
||||
func (r *Reflector) extractGoComments(base, path string, commentMap map[string]string, opts *commentOptions) error {
|
||||
fset := token.NewFileSet()
|
||||
dict := make(map[string][]*ast.Package)
|
||||
err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
d, err := parser.ParseDir(fset, path, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, v := range d {
|
||||
// paths may have multiple packages, like for tests
|
||||
k := gopath.Join(base, path)
|
||||
dict[k] = append(dict[k], v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for pkg, p := range dict {
|
||||
for _, f := range p {
|
||||
gtxt := ""
|
||||
typ := ""
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
switch x := n.(type) {
|
||||
case *ast.TypeSpec:
|
||||
typ = x.Name.String()
|
||||
if !ast.IsExported(typ) {
|
||||
typ = ""
|
||||
} else {
|
||||
txt := x.Doc.Text()
|
||||
if txt == "" && gtxt != "" {
|
||||
txt = gtxt
|
||||
gtxt = ""
|
||||
}
|
||||
if !opts.fullObjectText {
|
||||
txt = doc.Synopsis(txt)
|
||||
}
|
||||
commentMap[fmt.Sprintf("%s.%s", pkg, typ)] = strings.TrimSpace(txt)
|
||||
}
|
||||
case *ast.Field:
|
||||
txt := x.Doc.Text()
|
||||
if txt == "" {
|
||||
txt = x.Comment.Text()
|
||||
}
|
||||
if typ != "" && txt != "" {
|
||||
for _, n := range x.Names {
|
||||
if ast.IsExported(n.String()) {
|
||||
k := fmt.Sprintf("%s.%s.%s", pkg, typ, n)
|
||||
commentMap[k] = strings.TrimSpace(txt)
|
||||
}
|
||||
}
|
||||
}
|
||||
case *ast.GenDecl:
|
||||
// remember for the next type
|
||||
gtxt = x.Doc.Text()
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reflector) lookupComment(t reflect.Type, name string) string {
|
||||
if r.LookupComment != nil {
|
||||
if comment := r.LookupComment(t, name); comment != "" {
|
||||
return comment
|
||||
}
|
||||
}
|
||||
|
||||
if r.CommentMap == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
n := fullyQualifiedTypeName(t)
|
||||
if name != "" {
|
||||
n = n + "." + name
|
||||
}
|
||||
|
||||
return r.CommentMap[n]
|
||||
}
|
||||
94
vendor/github.com/invopop/jsonschema/schema.go
generated
vendored
94
vendor/github.com/invopop/jsonschema/schema.go
generated
vendored
@@ -1,94 +0,0 @@
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
)
|
||||
|
||||
// Version is the JSON Schema version.
|
||||
var Version = "https://json-schema.org/draft/2020-12/schema"
|
||||
|
||||
// Schema represents a JSON Schema object type.
|
||||
// RFC draft-bhutton-json-schema-00 section 4.3
|
||||
type Schema struct {
|
||||
// RFC draft-bhutton-json-schema-00
|
||||
Version string `json:"$schema,omitempty"` // section 8.1.1
|
||||
ID ID `json:"$id,omitempty"` // section 8.2.1
|
||||
Anchor string `json:"$anchor,omitempty"` // section 8.2.2
|
||||
Ref string `json:"$ref,omitempty"` // section 8.2.3.1
|
||||
DynamicRef string `json:"$dynamicRef,omitempty"` // section 8.2.3.2
|
||||
Definitions Definitions `json:"$defs,omitempty"` // section 8.2.4
|
||||
Comments string `json:"$comment,omitempty"` // section 8.3
|
||||
// RFC draft-bhutton-json-schema-00 section 10.2.1 (Sub-schemas with logic)
|
||||
AllOf []*Schema `json:"allOf,omitempty"` // section 10.2.1.1
|
||||
AnyOf []*Schema `json:"anyOf,omitempty"` // section 10.2.1.2
|
||||
OneOf []*Schema `json:"oneOf,omitempty"` // section 10.2.1.3
|
||||
Not *Schema `json:"not,omitempty"` // section 10.2.1.4
|
||||
// RFC draft-bhutton-json-schema-00 section 10.2.2 (Apply sub-schemas conditionally)
|
||||
If *Schema `json:"if,omitempty"` // section 10.2.2.1
|
||||
Then *Schema `json:"then,omitempty"` // section 10.2.2.2
|
||||
Else *Schema `json:"else,omitempty"` // section 10.2.2.3
|
||||
DependentSchemas map[string]*Schema `json:"dependentSchemas,omitempty"` // section 10.2.2.4
|
||||
// RFC draft-bhutton-json-schema-00 section 10.3.1 (arrays)
|
||||
PrefixItems []*Schema `json:"prefixItems,omitempty"` // section 10.3.1.1
|
||||
Items *Schema `json:"items,omitempty"` // section 10.3.1.2 (replaces additionalItems)
|
||||
Contains *Schema `json:"contains,omitempty"` // section 10.3.1.3
|
||||
// RFC draft-bhutton-json-schema-00 section 10.3.2 (sub-schemas)
|
||||
Properties *orderedmap.OrderedMap[string, *Schema] `json:"properties,omitempty"` // section 10.3.2.1
|
||||
PatternProperties map[string]*Schema `json:"patternProperties,omitempty"` // section 10.3.2.2
|
||||
AdditionalProperties *Schema `json:"additionalProperties,omitempty"` // section 10.3.2.3
|
||||
PropertyNames *Schema `json:"propertyNames,omitempty"` // section 10.3.2.4
|
||||
// RFC draft-bhutton-json-schema-validation-00, section 6
|
||||
Type string `json:"type,omitempty"` // section 6.1.1
|
||||
Enum []any `json:"enum,omitempty"` // section 6.1.2
|
||||
Const any `json:"const,omitempty"` // section 6.1.3
|
||||
MultipleOf json.Number `json:"multipleOf,omitempty"` // section 6.2.1
|
||||
Maximum json.Number `json:"maximum,omitempty"` // section 6.2.2
|
||||
ExclusiveMaximum json.Number `json:"exclusiveMaximum,omitempty"` // section 6.2.3
|
||||
Minimum json.Number `json:"minimum,omitempty"` // section 6.2.4
|
||||
ExclusiveMinimum json.Number `json:"exclusiveMinimum,omitempty"` // section 6.2.5
|
||||
MaxLength *uint64 `json:"maxLength,omitempty"` // section 6.3.1
|
||||
MinLength *uint64 `json:"minLength,omitempty"` // section 6.3.2
|
||||
Pattern string `json:"pattern,omitempty"` // section 6.3.3
|
||||
MaxItems *uint64 `json:"maxItems,omitempty"` // section 6.4.1
|
||||
MinItems *uint64 `json:"minItems,omitempty"` // section 6.4.2
|
||||
UniqueItems bool `json:"uniqueItems,omitempty"` // section 6.4.3
|
||||
MaxContains *uint64 `json:"maxContains,omitempty"` // section 6.4.4
|
||||
MinContains *uint64 `json:"minContains,omitempty"` // section 6.4.5
|
||||
MaxProperties *uint64 `json:"maxProperties,omitempty"` // section 6.5.1
|
||||
MinProperties *uint64 `json:"minProperties,omitempty"` // section 6.5.2
|
||||
Required []string `json:"required,omitempty"` // section 6.5.3
|
||||
DependentRequired map[string][]string `json:"dependentRequired,omitempty"` // section 6.5.4
|
||||
// RFC draft-bhutton-json-schema-validation-00, section 7
|
||||
Format string `json:"format,omitempty"`
|
||||
// RFC draft-bhutton-json-schema-validation-00, section 8
|
||||
ContentEncoding string `json:"contentEncoding,omitempty"` // section 8.3
|
||||
ContentMediaType string `json:"contentMediaType,omitempty"` // section 8.4
|
||||
ContentSchema *Schema `json:"contentSchema,omitempty"` // section 8.5
|
||||
// RFC draft-bhutton-json-schema-validation-00, section 9
|
||||
Title string `json:"title,omitempty"` // section 9.1
|
||||
Description string `json:"description,omitempty"` // section 9.1
|
||||
Default any `json:"default,omitempty"` // section 9.2
|
||||
Deprecated bool `json:"deprecated,omitempty"` // section 9.3
|
||||
ReadOnly bool `json:"readOnly,omitempty"` // section 9.4
|
||||
WriteOnly bool `json:"writeOnly,omitempty"` // section 9.4
|
||||
Examples []any `json:"examples,omitempty"` // section 9.5
|
||||
|
||||
Extras map[string]any `json:"-"`
|
||||
|
||||
// Special boolean representation of the Schema - section 4.3.2
|
||||
boolean *bool
|
||||
}
|
||||
|
||||
var (
|
||||
// TrueSchema defines a schema with a true value
|
||||
TrueSchema = &Schema{boolean: &[]bool{true}[0]}
|
||||
// FalseSchema defines a schema with a false value
|
||||
FalseSchema = &Schema{boolean: &[]bool{false}[0]}
|
||||
)
|
||||
|
||||
// Definitions hold schema definitions.
|
||||
// http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.26
|
||||
// RFC draft-wright-json-schema-validation-00, section 5.26
|
||||
type Definitions map[string]*Schema
|
||||
26
vendor/github.com/invopop/jsonschema/utils.go
generated
vendored
26
vendor/github.com/invopop/jsonschema/utils.go
generated
vendored
@@ -1,26 +0,0 @@
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
)
|
||||
|
||||
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
|
||||
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
|
||||
|
||||
// ToSnakeCase converts the provided string into snake case using dashes.
|
||||
// This is useful for Schema IDs and definitions to be coherent with
|
||||
// common JSON Schema examples.
|
||||
func ToSnakeCase(str string) string {
|
||||
snake := matchFirstCap.ReplaceAllString(str, "${1}-${2}")
|
||||
snake = matchAllCap.ReplaceAllString(snake, "${1}-${2}")
|
||||
return strings.ToLower(snake)
|
||||
}
|
||||
|
||||
// NewProperties is a helper method to instantiate a new properties ordered
|
||||
// map.
|
||||
func NewProperties() *orderedmap.OrderedMap[string, *Schema] {
|
||||
return orderedmap.New[string, *Schema]()
|
||||
}
|
||||
3
vendor/github.com/mark3labs/mcp-go/mcp/consts.go
generated
vendored
3
vendor/github.com/mark3labs/mcp-go/mcp/consts.go
generated
vendored
@@ -6,4 +6,7 @@ const (
|
||||
ContentTypeAudio = "audio"
|
||||
ContentTypeLink = "resource_link"
|
||||
ContentTypeResource = "resource"
|
||||
|
||||
ElicitationModeForm = "form"
|
||||
ElicitationModeURL = "url"
|
||||
)
|
||||
|
||||
109
vendor/github.com/mark3labs/mcp-go/mcp/errors.go
generated
vendored
109
vendor/github.com/mark3labs/mcp-go/mcp/errors.go
generated
vendored
@@ -1,6 +1,56 @@
|
||||
package mcp
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Sentinel errors for common JSON-RPC error codes.
|
||||
var (
|
||||
// ErrParseError indicates a JSON parsing error (code: PARSE_ERROR).
|
||||
ErrParseError = errors.New("parse error")
|
||||
|
||||
// ErrInvalidRequest indicates an invalid JSON-RPC request (code: INVALID_REQUEST).
|
||||
ErrInvalidRequest = errors.New("invalid request")
|
||||
|
||||
// ErrMethodNotFound indicates the requested method does not exist (code: METHOD_NOT_FOUND).
|
||||
ErrMethodNotFound = errors.New("method not found")
|
||||
|
||||
// ErrInvalidParams indicates invalid method parameters (code: INVALID_PARAMS).
|
||||
ErrInvalidParams = errors.New("invalid params")
|
||||
|
||||
// ErrInternalError indicates an internal JSON-RPC error (code: INTERNAL_ERROR).
|
||||
ErrInternalError = errors.New("internal error")
|
||||
|
||||
// ErrRequestInterrupted indicates a request was cancelled or timed out (code: REQUEST_INTERRUPTED).
|
||||
ErrRequestInterrupted = errors.New("request interrupted")
|
||||
|
||||
// ErrResourceNotFound indicates a requested resource was not found (code: RESOURCE_NOT_FOUND).
|
||||
ErrResourceNotFound = errors.New("resource not found")
|
||||
)
|
||||
|
||||
// URLElicitationRequiredError is returned when the server requires URL elicitation to proceed.
|
||||
type URLElicitationRequiredError struct {
|
||||
Elicitations []ElicitationParams `json:"elicitations"`
|
||||
}
|
||||
|
||||
func (e URLElicitationRequiredError) Error() string {
|
||||
return fmt.Sprintf("URL elicitation required: %d elicitation(s) needed", len(e.Elicitations))
|
||||
}
|
||||
|
||||
func (e URLElicitationRequiredError) JSONRPCError() JSONRPCError {
|
||||
return JSONRPCError{
|
||||
JSONRPC: JSONRPC_VERSION,
|
||||
Error: JSONRPCErrorDetails{
|
||||
Code: URL_ELICITATION_REQUIRED,
|
||||
Message: e.Error(),
|
||||
Data: map[string]any{
|
||||
"elicitations": e.Elicitations,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// UnsupportedProtocolVersionError is returned when the server responds with
|
||||
// a protocol version that the client doesn't support.
|
||||
@@ -12,6 +62,12 @@ func (e UnsupportedProtocolVersionError) Error() string {
|
||||
return fmt.Sprintf("unsupported protocol version: %q", e.Version)
|
||||
}
|
||||
|
||||
// Is implements the errors.Is interface for better error handling
|
||||
func (e URLElicitationRequiredError) Is(target error) bool {
|
||||
_, ok := target.(URLElicitationRequiredError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Is implements the errors.Is interface for better error handling
|
||||
func (e UnsupportedProtocolVersionError) Is(target error) bool {
|
||||
_, ok := target.(UnsupportedProtocolVersionError)
|
||||
@@ -23,3 +79,54 @@ func IsUnsupportedProtocolVersion(err error) bool {
|
||||
_, ok := err.(UnsupportedProtocolVersionError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// AsError maps JSONRPCErrorDetails to a Go error.
|
||||
// Returns sentinel errors wrapped with custom messages for known codes.
|
||||
// Defaults to a generic error with the original message when the code is not mapped.
|
||||
func (e *JSONRPCErrorDetails) AsError() error {
|
||||
var err error
|
||||
|
||||
switch e.Code {
|
||||
case PARSE_ERROR:
|
||||
err = ErrParseError
|
||||
case INVALID_REQUEST:
|
||||
err = ErrInvalidRequest
|
||||
case METHOD_NOT_FOUND:
|
||||
err = ErrMethodNotFound
|
||||
case INVALID_PARAMS:
|
||||
err = ErrInvalidParams
|
||||
case INTERNAL_ERROR:
|
||||
err = ErrInternalError
|
||||
case REQUEST_INTERRUPTED:
|
||||
err = ErrRequestInterrupted
|
||||
case RESOURCE_NOT_FOUND:
|
||||
err = ErrResourceNotFound
|
||||
case URL_ELICITATION_REQUIRED:
|
||||
// Attempt to reconstruct URLElicitationRequiredError from Data
|
||||
if e.Data != nil {
|
||||
// Round-trip through JSON to parse into struct
|
||||
// This handles both map[string]any (from unmarshal) and other forms
|
||||
if dataBytes, marshalErr := json.Marshal(e.Data); marshalErr == nil {
|
||||
var data struct {
|
||||
Elicitations []ElicitationParams `json:"elicitations"`
|
||||
}
|
||||
if unmarshalErr := json.Unmarshal(dataBytes, &data); unmarshalErr == nil {
|
||||
return URLElicitationRequiredError{
|
||||
Elicitations: data.Elicitations,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback if data is missing or invalid
|
||||
return URLElicitationRequiredError{}
|
||||
default:
|
||||
return errors.New(e.Message)
|
||||
}
|
||||
|
||||
// Wrap the sentinel error with the custom message if it differs from the sentinel.
|
||||
if e.Message != "" && e.Message != err.Error() {
|
||||
return fmt.Errorf("%w: %s", err, e.Message)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
10
vendor/github.com/mark3labs/mcp-go/mcp/prompts.go
generated
vendored
10
vendor/github.com/mark3labs/mcp-go/mcp/prompts.go
generated
vendored
@@ -56,6 +56,8 @@ type Prompt struct {
|
||||
// A list of arguments to use for templating the prompt.
|
||||
// The presence of arguments indicates this is a template prompt.
|
||||
Arguments []PromptArgument `json:"arguments,omitempty"`
|
||||
// Icons provides visual identifiers for the prompt
|
||||
Icons []Icon `json:"icons,omitempty"`
|
||||
}
|
||||
|
||||
// GetName returns the name of the prompt.
|
||||
@@ -136,6 +138,14 @@ func WithPromptDescription(description string) PromptOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithPromptIcons adds icons to the Prompt.
|
||||
// Icons provide visual identifiers for the prompt.
|
||||
func WithPromptIcons(icons ...Icon) PromptOption {
|
||||
return func(p *Prompt) {
|
||||
p.Icons = icons
|
||||
}
|
||||
}
|
||||
|
||||
// WithArgument adds an argument to the prompt's argument list.
|
||||
// The argument will be configured based on the provided options.
|
||||
func WithArgument(name string, opts ...ArgumentOption) PromptOption {
|
||||
|
||||
65
vendor/github.com/mark3labs/mcp-go/mcp/resources.go
generated
vendored
65
vendor/github.com/mark3labs/mcp-go/mcp/resources.go
generated
vendored
@@ -1,6 +1,10 @@
|
||||
package mcp
|
||||
|
||||
import "github.com/yosida95/uritemplate/v3"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/yosida95/uritemplate/v3"
|
||||
)
|
||||
|
||||
// ResourceOption is a function that configures a Resource.
|
||||
// It provides a flexible way to set various properties of a Resource using the functional options pattern.
|
||||
@@ -38,15 +42,29 @@ func WithMIMEType(mimeType string) ResourceOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithAnnotations adds annotations to the Resource.
|
||||
// Annotations can provide additional metadata about the resource's intended use.
|
||||
func WithAnnotations(audience []Role, priority float64) ResourceOption {
|
||||
// WithAnnotations returns a ResourceOption that sets the resource's Annotations fields.
|
||||
// It initializes Annotations if nil, sets Audience to the provided slice,
|
||||
// stores Priority as a pointer to the provided value, and sets LastModified to the provided timestamp.
|
||||
func WithAnnotations(audience []Role, priority float64, lastModified string) ResourceOption {
|
||||
return func(r *Resource) {
|
||||
if r.Annotations == nil {
|
||||
r.Annotations = &Annotations{}
|
||||
}
|
||||
r.Annotations.Audience = audience
|
||||
r.Annotations.Priority = priority
|
||||
r.Annotations.Priority = &priority
|
||||
r.Annotations.LastModified = lastModified
|
||||
}
|
||||
}
|
||||
|
||||
// WithLastModified returns a ResourceOption that sets the resource's Annotations.LastModified
|
||||
// to the provided timestamp. If the resource's Annotations is nil, it will be initialized.
|
||||
// The timestamp is expected to be an ISO 8601 (RFC3339) formatted string (e.g., "2025-01-12T15:00:58Z").
|
||||
func WithLastModified(timestamp string) ResourceOption {
|
||||
return func(r *Resource) {
|
||||
if r.Annotations == nil {
|
||||
r.Annotations = &Annotations{}
|
||||
}
|
||||
r.Annotations.LastModified = timestamp
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,14 +104,43 @@ func WithTemplateMIMEType(mimeType string) ResourceTemplateOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithTemplateAnnotations adds annotations to the ResourceTemplate.
|
||||
// Annotations can provide additional metadata about the template's intended use.
|
||||
func WithTemplateAnnotations(audience []Role, priority float64) ResourceTemplateOption {
|
||||
// WithTemplateAnnotations returns a ResourceTemplateOption that sets the template's
|
||||
// Annotations field, initializing it if nil, and setting Audience, Priority, and LastModified.
|
||||
func WithTemplateAnnotations(audience []Role, priority float64, lastModified string) ResourceTemplateOption {
|
||||
return func(t *ResourceTemplate) {
|
||||
if t.Annotations == nil {
|
||||
t.Annotations = &Annotations{}
|
||||
}
|
||||
t.Annotations.Audience = audience
|
||||
t.Annotations.Priority = priority
|
||||
t.Annotations.Priority = &priority
|
||||
t.Annotations.LastModified = lastModified
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateISO8601Timestamp verifies that timestamp is a valid ISO 8601 timestamp
|
||||
// using the RFC3339 layout. An empty string is considered valid. It returns nil
|
||||
// when the timestamp is valid, or the parsing error when it is not.
|
||||
func ValidateISO8601Timestamp(timestamp string) error {
|
||||
if timestamp == "" {
|
||||
return nil // Empty is valid (optional field)
|
||||
}
|
||||
// Use time.RFC3339 for ISO 8601 compatibility
|
||||
_, err := time.Parse(time.RFC3339, timestamp)
|
||||
return err
|
||||
}
|
||||
|
||||
// WithResourceIcons adds icons to the Resource.
|
||||
// Icons provide visual identifiers for the resource.
|
||||
func WithResourceIcons(icons ...Icon) ResourceOption {
|
||||
return func(r *Resource) {
|
||||
r.Icons = icons
|
||||
}
|
||||
}
|
||||
|
||||
// WithTemplateIcons adds icons to the ResourceTemplate.
|
||||
// Icons provide visual identifiers for the resource template.
|
||||
func WithTemplateIcons(icons ...Icon) ResourceTemplateOption {
|
||||
return func(rt *ResourceTemplate) {
|
||||
rt.Icons = icons
|
||||
}
|
||||
}
|
||||
|
||||
208
vendor/github.com/mark3labs/mcp-go/mcp/tasks.go
generated
vendored
Normal file
208
vendor/github.com/mark3labs/mcp-go/mcp/tasks.go
generated
vendored
Normal file
@@ -0,0 +1,208 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// TaskOption is a function that configures a Task.
|
||||
// It provides a flexible way to set various properties of a Task using the functional options pattern.
|
||||
type TaskOption func(*Task)
|
||||
|
||||
//
|
||||
// Core Task Functions
|
||||
//
|
||||
|
||||
// NewTask creates a new Task with the given ID and options.
|
||||
// The task will be configured based on the provided options.
|
||||
// Options are applied in order, allowing for flexible task configuration.
|
||||
func NewTask(taskId string, opts ...TaskOption) Task {
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
task := Task{
|
||||
TaskId: taskId,
|
||||
Status: TaskStatusWorking,
|
||||
CreatedAt: now,
|
||||
LastUpdatedAt: now,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(&task)
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
// WithTaskStatus sets the status of the task.
|
||||
func WithTaskStatus(status TaskStatus) TaskOption {
|
||||
return func(t *Task) {
|
||||
t.Status = status
|
||||
}
|
||||
}
|
||||
|
||||
// WithTaskStatusMessage sets a human-readable status message for the task.
|
||||
func WithTaskStatusMessage(message string) TaskOption {
|
||||
return func(t *Task) {
|
||||
t.StatusMessage = message
|
||||
}
|
||||
}
|
||||
|
||||
// WithTaskTTL sets the time-to-live for the task in milliseconds.
|
||||
// After this duration from creation, the task may be deleted.
|
||||
func WithTaskTTL(ttlMs int64) TaskOption {
|
||||
return func(t *Task) {
|
||||
t.TTL = &ttlMs
|
||||
}
|
||||
}
|
||||
|
||||
// WithTaskPollInterval sets the suggested polling interval in milliseconds.
|
||||
func WithTaskPollInterval(intervalMs int64) TaskOption {
|
||||
return func(t *Task) {
|
||||
t.PollInterval = &intervalMs
|
||||
}
|
||||
}
|
||||
|
||||
// WithTaskCreatedAt sets a specific creation timestamp for the task.
|
||||
// By default, NewTask uses the current time.
|
||||
func WithTaskCreatedAt(createdAt string) TaskOption {
|
||||
return func(t *Task) {
|
||||
t.CreatedAt = createdAt
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Task Helper Functions
|
||||
//
|
||||
|
||||
// NewTaskParams creates TaskParams with the given TTL.
|
||||
func NewTaskParams(ttlMs *int64) TaskParams {
|
||||
return TaskParams{
|
||||
TTL: ttlMs,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCreateTaskResult creates a CreateTaskResult with the given task.
|
||||
func NewCreateTaskResult(task Task) CreateTaskResult {
|
||||
return CreateTaskResult{
|
||||
Task: task,
|
||||
}
|
||||
}
|
||||
|
||||
// NewGetTaskResult creates a GetTaskResult from a Task.
|
||||
func NewGetTaskResult(task Task) GetTaskResult {
|
||||
return GetTaskResult{
|
||||
Task: task,
|
||||
}
|
||||
}
|
||||
|
||||
// NewListTasksResult creates a ListTasksResult with the given tasks.
|
||||
func NewListTasksResult(tasks []Task) ListTasksResult {
|
||||
return ListTasksResult{
|
||||
Tasks: tasks,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCancelTaskResult creates a CancelTaskResult from a Task.
|
||||
func NewCancelTaskResult(task Task) CancelTaskResult {
|
||||
return CancelTaskResult{
|
||||
Task: task,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTaskStatusNotification creates a notification for a task status change.
|
||||
func NewTaskStatusNotification(task Task) TaskStatusNotification {
|
||||
return TaskStatusNotification{
|
||||
Notification: Notification{
|
||||
Method: string(MethodNotificationTasksStatus),
|
||||
},
|
||||
Params: TaskStatusNotificationParams{
|
||||
Task: task,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Task Capability Helper Functions
|
||||
//
|
||||
|
||||
// NewTasksCapability creates a TasksCapability with all operations enabled.
|
||||
func NewTasksCapability() *TasksCapability {
|
||||
return &TasksCapability{
|
||||
List: &struct{}{},
|
||||
Cancel: &struct{}{},
|
||||
Requests: &TaskRequestsCapability{
|
||||
Tools: &struct {
|
||||
Call *struct{} `json:"call,omitempty"`
|
||||
}{
|
||||
Call: &struct{}{},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewTasksCapabilityWithToolsOnly creates a TasksCapability with only tool call support.
|
||||
// List and Cancel operations are not enabled with this capability.
|
||||
func NewTasksCapabilityWithToolsOnly() *TasksCapability {
|
||||
return &TasksCapability{
|
||||
Requests: &TaskRequestsCapability{
|
||||
Tools: &struct {
|
||||
Call *struct{} `json:"call,omitempty"`
|
||||
}{
|
||||
Call: &struct{}{},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Related Task Metadata Functions
|
||||
//
|
||||
|
||||
// RelatedTaskMetaKey is the metadata key for associating a message with a task.
|
||||
const RelatedTaskMetaKey = "io.modelcontextprotocol/related-task"
|
||||
|
||||
// RelatedTaskMeta creates the metadata for associating a message with a task.
|
||||
// The returned map contains a "taskId" field with the provided task ID.
|
||||
func RelatedTaskMeta(taskID string) map[string]any {
|
||||
return map[string]any{
|
||||
"taskId": taskID,
|
||||
}
|
||||
}
|
||||
|
||||
// WithRelatedTask returns a Meta with the related task ID set.
|
||||
// This is useful for associating task results with their originating task.
|
||||
func WithRelatedTask(taskID string) *Meta {
|
||||
return &Meta{
|
||||
AdditionalFields: map[string]any{
|
||||
RelatedTaskMetaKey: RelatedTaskMeta(taskID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Model Immediate Response Metadata Functions
|
||||
//
|
||||
|
||||
// ModelImmediateResponseMetaKey is the metadata key for providing an immediate response to the model.
|
||||
// Servers can use this optional key in the _meta field of CreateTaskResult to provide
|
||||
// a string that should be passed as an immediate tool result to the model while the task
|
||||
// continues executing asynchronously in the background.
|
||||
const ModelImmediateResponseMetaKey = "io.modelcontextprotocol/model-immediate-response"
|
||||
|
||||
// WithModelImmediateResponse creates Meta with an immediate response message for the model.
|
||||
// This allows the model to continue processing while the task executes asynchronously.
|
||||
// The message parameter is a human-readable string that will be shown to the model.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// return &mcp.CreateTaskResult{
|
||||
// Task: task,
|
||||
// Result: mcp.Result{
|
||||
// Meta: mcp.WithModelImmediateResponse("Processing your request. This may take a few minutes."),
|
||||
// },
|
||||
// }
|
||||
func WithModelImmediateResponse(message string) *Meta {
|
||||
return &Meta{
|
||||
AdditionalFields: map[string]any{
|
||||
ModelImmediateResponseMetaKey: message,
|
||||
},
|
||||
}
|
||||
}
|
||||
231
vendor/github.com/mark3labs/mcp-go/mcp/tools.go
generated
vendored
231
vendor/github.com/mark3labs/mcp-go/mcp/tools.go
generated
vendored
@@ -8,7 +8,7 @@ import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/google/jsonschema-go/jsonschema"
|
||||
)
|
||||
|
||||
var errToolSchemaConflict = errors.New("provide either InputSchema or RawInputSchema, not both")
|
||||
@@ -58,9 +58,10 @@ type CallToolRequest struct {
|
||||
}
|
||||
|
||||
type CallToolParams struct {
|
||||
Name string `json:"name"`
|
||||
Arguments any `json:"arguments,omitempty"`
|
||||
Meta *Meta `json:"_meta,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Arguments any `json:"arguments,omitempty"`
|
||||
Meta *Meta `json:"_meta,omitempty"`
|
||||
Task *TaskParams `json:"task,omitempty"`
|
||||
}
|
||||
|
||||
// GetArguments returns the Arguments as map[string]any for backward compatibility
|
||||
@@ -553,6 +554,24 @@ type ToolListChangedNotification struct {
|
||||
Notification
|
||||
}
|
||||
|
||||
// TaskSupport indicates how a tool supports task augmentation.
|
||||
type TaskSupport string
|
||||
|
||||
const (
|
||||
// TaskSupportForbidden means the tool cannot be invoked as a task (default).
|
||||
TaskSupportForbidden TaskSupport = "forbidden"
|
||||
// TaskSupportOptional means the tool can be invoked as a task or normally.
|
||||
TaskSupportOptional TaskSupport = "optional"
|
||||
// TaskSupportRequired means the tool must be invoked as a task.
|
||||
TaskSupportRequired TaskSupport = "required"
|
||||
)
|
||||
|
||||
// ToolExecution describes execution behavior for a tool.
|
||||
type ToolExecution struct {
|
||||
// TaskSupport indicates whether the tool supports task augmentation.
|
||||
TaskSupport TaskSupport `json:"taskSupport,omitempty"`
|
||||
}
|
||||
|
||||
// Tool represents the definition for a tool the client can call.
|
||||
type Tool struct {
|
||||
// Meta is a metadata object that is reserved by MCP for storing additional information.
|
||||
@@ -571,6 +590,12 @@ type Tool struct {
|
||||
RawOutputSchema json.RawMessage `json:"-"` // Hide this from JSON marshaling
|
||||
// Optional properties describing tool behavior
|
||||
Annotations ToolAnnotation `json:"annotations"`
|
||||
// Support for deferred loading
|
||||
DeferLoading bool `json:"defer_loading,omitempty"`
|
||||
// Icons provides visual identifiers for the tool
|
||||
Icons []Icon `json:"icons,omitempty"`
|
||||
// Execution describes execution behavior for the tool
|
||||
Execution *ToolExecution `json:"execution,omitempty"`
|
||||
}
|
||||
|
||||
// GetName returns the name of the tool.
|
||||
@@ -613,22 +638,70 @@ func (t Tool) MarshalJSON() ([]byte, error) {
|
||||
|
||||
m["annotations"] = t.Annotations
|
||||
|
||||
if t.DeferLoading {
|
||||
m["defer_loading"] = t.DeferLoading
|
||||
}
|
||||
|
||||
// Marshal Meta if present
|
||||
if t.Meta != nil {
|
||||
m["_meta"] = t.Meta
|
||||
}
|
||||
|
||||
if t.Icons != nil {
|
||||
m["icons"] = t.Icons
|
||||
}
|
||||
|
||||
if t.Execution != nil {
|
||||
m["execution"] = t.Execution
|
||||
}
|
||||
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
// ToolArgumentsSchema represents a JSON Schema for tool arguments.
|
||||
type ToolArgumentsSchema struct {
|
||||
Defs map[string]any `json:"$defs,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Properties map[string]any `json:"properties,omitempty"`
|
||||
Required []string `json:"required,omitempty"`
|
||||
Defs map[string]any `json:"$defs,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Properties map[string]any `json:"properties"`
|
||||
Required []string `json:"required,omitempty"`
|
||||
AdditionalProperties any `json:"additionalProperties,omitempty"`
|
||||
}
|
||||
|
||||
type ToolInputSchema ToolArgumentsSchema // For retro-compatibility
|
||||
type ToolOutputSchema ToolArgumentsSchema
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for ToolInputSchema.
|
||||
func (tis ToolInputSchema) MarshalJSON() ([]byte, error) {
|
||||
return toolArgumentsSchemaMarshalJSON(ToolArgumentsSchema(tis))
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for ToolOutputSchema.
|
||||
func (tis ToolOutputSchema) MarshalJSON() ([]byte, error) {
|
||||
return toolArgumentsSchemaMarshalJSON(ToolArgumentsSchema(tis))
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for ToolArgumentsSchema.
|
||||
func (tis ToolArgumentsSchema) MarshalJSON() ([]byte, error) {
|
||||
return toolArgumentsSchemaMarshalJSON(tis)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for ToolInputSchema.
|
||||
func (tis *ToolInputSchema) UnmarshalJSON(data []byte) error {
|
||||
return toolArgumentsSchemaUnmarshalJSON(data, (*ToolArgumentsSchema)(tis))
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for ToolOutputSchema.
|
||||
func (tis *ToolOutputSchema) UnmarshalJSON(data []byte) error {
|
||||
return toolArgumentsSchemaUnmarshalJSON(data, (*ToolArgumentsSchema)(tis))
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for ToolArgumentsSchema.
|
||||
func (tis *ToolArgumentsSchema) UnmarshalJSON(data []byte) error {
|
||||
return toolArgumentsSchemaUnmarshalJSON(data, tis)
|
||||
}
|
||||
|
||||
// toolArgumentsSchemaMarshalJSON handles the fields stored in ToolArgumentsSchema when json.Marshaler is called
|
||||
func toolArgumentsSchemaMarshalJSON(tis ToolArgumentsSchema) ([]byte, error) {
|
||||
m := make(map[string]any)
|
||||
m["type"] = tis.Type
|
||||
|
||||
@@ -639,15 +712,48 @@ func (tis ToolArgumentsSchema) MarshalJSON() ([]byte, error) {
|
||||
// Marshal Properties to '{}' rather than `nil` when its length equals zero
|
||||
if tis.Properties != nil {
|
||||
m["properties"] = tis.Properties
|
||||
} else {
|
||||
m["properties"] = map[string]any{}
|
||||
}
|
||||
|
||||
// Marshal Required to '[]' rather than `nil` when its length equals zero
|
||||
if len(tis.Required) > 0 {
|
||||
m["required"] = tis.Required
|
||||
} else {
|
||||
m["required"] = []string{}
|
||||
}
|
||||
|
||||
if tis.AdditionalProperties != nil {
|
||||
m["additionalProperties"] = tis.AdditionalProperties
|
||||
}
|
||||
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
// It handles both "$defs" (JSON Schema 2019-09+) and "definitions" (JSON Schema draft-07)
|
||||
// by reading either field and storing it in the Defs field.
|
||||
func toolArgumentsSchemaUnmarshalJSON(data []byte, tis *ToolArgumentsSchema) error {
|
||||
// Use a temporary type to avoid infinite recursion
|
||||
type Alias ToolArgumentsSchema
|
||||
aux := &struct {
|
||||
Definitions map[string]any `json:"definitions,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(tis),
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If $defs wasn't provided but definitions was, use definitions
|
||||
if tis.Defs == nil && aux.Definitions != nil {
|
||||
tis.Defs = aux.Definitions
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ToolAnnotation struct {
|
||||
// Human-readable title for the tool
|
||||
Title string `json:"title,omitempty"`
|
||||
@@ -725,28 +831,25 @@ func WithDescription(description string) ToolOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithDeferLoading sets the defer_loading flag for the tool.
|
||||
// This is used to implement dynamic tool loading/searching patterns.
|
||||
func WithDeferLoading(deferLoading bool) ToolOption {
|
||||
return func(t *Tool) {
|
||||
t.DeferLoading = deferLoading
|
||||
}
|
||||
}
|
||||
|
||||
// WithInputSchema creates a ToolOption that sets the input schema for a tool.
|
||||
// It accepts any Go type, usually a struct, and automatically generates a JSON schema from it.
|
||||
func WithInputSchema[T any]() ToolOption {
|
||||
return func(t *Tool) {
|
||||
var zero T
|
||||
|
||||
// Generate schema using invopop/jsonschema library
|
||||
// Configure reflector to generate clean, MCP-compatible schemas
|
||||
reflector := jsonschema.Reflector{
|
||||
DoNotReference: true, // Removes $defs map, outputs entire structure inline
|
||||
Anonymous: true, // Hides auto-generated Schema IDs
|
||||
AllowAdditionalProperties: true, // Removes additionalProperties: false
|
||||
schema, err := jsonschema.For[T](&jsonschema.ForOptions{IgnoreInvalidTypes: true})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
schema := reflector.Reflect(zero)
|
||||
|
||||
// Clean up schema for MCP compliance
|
||||
schema.Version = "" // Remove $schema field
|
||||
|
||||
// Convert to raw JSON for MCP
|
||||
mcpSchema, err := json.Marshal(schema)
|
||||
if err != nil {
|
||||
// Skip and maintain backward compatibility
|
||||
return
|
||||
}
|
||||
|
||||
@@ -755,6 +858,26 @@ func WithInputSchema[T any]() ToolOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithToolIcons adds icons to the Tool.
|
||||
// Icons provide visual identifiers for the tool.
|
||||
func WithToolIcons(icons ...Icon) ToolOption {
|
||||
return func(t *Tool) {
|
||||
t.Icons = icons
|
||||
}
|
||||
}
|
||||
|
||||
// WithTaskSupport sets the task support mode for the tool.
|
||||
// It configures whether the tool can be invoked as a task (asynchronously).
|
||||
// Valid values are TaskSupportForbidden (default), TaskSupportOptional, or TaskSupportRequired.
|
||||
func WithTaskSupport(support TaskSupport) ToolOption {
|
||||
return func(t *Tool) {
|
||||
if t.Execution == nil {
|
||||
t.Execution = &ToolExecution{}
|
||||
}
|
||||
t.Execution.TaskSupport = support
|
||||
}
|
||||
}
|
||||
|
||||
// WithRawInputSchema sets a raw JSON schema for the tool's input.
|
||||
// Use this when you need full control over the schema or when working with
|
||||
// complex schemas that can't be generated from Go types. The jsonschema library
|
||||
@@ -770,30 +893,17 @@ func WithRawInputSchema(schema json.RawMessage) ToolOption {
|
||||
// It accepts any Go type, usually a struct, and automatically generates a JSON schema from it.
|
||||
func WithOutputSchema[T any]() ToolOption {
|
||||
return func(t *Tool) {
|
||||
var zero T
|
||||
|
||||
// Generate schema using invopop/jsonschema library
|
||||
// Configure reflector to generate clean, MCP-compatible schemas
|
||||
reflector := jsonschema.Reflector{
|
||||
DoNotReference: true, // Removes $defs map, outputs entire structure inline
|
||||
Anonymous: true, // Hides auto-generated Schema IDs
|
||||
AllowAdditionalProperties: true, // Removes additionalProperties: false
|
||||
}
|
||||
schema := reflector.Reflect(zero)
|
||||
|
||||
// Clean up schema for MCP compliance
|
||||
schema.Version = "" // Remove $schema field
|
||||
|
||||
// Convert to raw JSON for MCP
|
||||
mcpSchema, err := json.Marshal(schema)
|
||||
schema, err := jsonschema.For[T](&jsonschema.ForOptions{IgnoreInvalidTypes: true})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mcpSchema, err := json.Marshal(schema)
|
||||
if err != nil {
|
||||
// Skip and maintain backward compatibility
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve the schema from raw JSON
|
||||
if err := json.Unmarshal(mcpSchema, &t.OutputSchema); err != nil {
|
||||
// Skip and maintain backward compatibility
|
||||
return
|
||||
}
|
||||
|
||||
@@ -861,6 +971,15 @@ func WithOpenWorldHintAnnotation(value bool) ToolOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithSchemaAdditionalProperties sets the additionalProperties field on the tool's input schema.
|
||||
// It accepts false (disallow extra properties), true (allow any), or a schema map
|
||||
// to validate additional properties against.
|
||||
func WithSchemaAdditionalProperties(schema any) ToolOption {
|
||||
return func(t *Tool) {
|
||||
t.InputSchema.AdditionalProperties = schema
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Common Property Options
|
||||
//
|
||||
@@ -1086,8 +1205,10 @@ func WithObject(name string, opts ...PropertyOption) ToolOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithArray adds an array property to the tool schema.
|
||||
// It accepts property options to configure the array property's behavior and constraints.
|
||||
// WithArray returns a ToolOption that adds an array-typed property with the given name to a Tool's input schema.
|
||||
// It applies provided PropertyOption functions to configure the property's schema, moves a `required` flag
|
||||
// from the property schema into the Tool's InputSchema.Required slice when present, and registers the resulting
|
||||
// schema under InputSchema.Properties[name].
|
||||
func WithArray(name string, opts ...PropertyOption) ToolOption {
|
||||
return func(t *Tool) {
|
||||
schema := map[string]any{
|
||||
@@ -1108,7 +1229,29 @@ func WithArray(name string, opts ...PropertyOption) ToolOption {
|
||||
}
|
||||
}
|
||||
|
||||
// Properties defines the properties for an object schema
|
||||
// WithAny adds an input property named name with no predefined JSON Schema type to the Tool's input schema.
|
||||
// The returned ToolOption applies the provided PropertyOption functions to the property's schema, moves a property-level
|
||||
// `required` flag into the Tool's InputSchema.Required list if present, and stores the resulting schema under InputSchema.Properties[name].
|
||||
func WithAny(name string, opts ...PropertyOption) ToolOption {
|
||||
return func(t *Tool) {
|
||||
schema := map[string]any{}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(schema)
|
||||
}
|
||||
|
||||
// Remove required from property schema and add to InputSchema.required
|
||||
if required, ok := schema["required"].(bool); ok && required {
|
||||
delete(schema, "required")
|
||||
t.InputSchema.Required = append(t.InputSchema.Required, name)
|
||||
}
|
||||
|
||||
t.InputSchema.Properties[name] = schema
|
||||
}
|
||||
}
|
||||
|
||||
// Properties sets the "properties" map for an object schema.
|
||||
// The returned PropertyOption stores the provided map under the schema's "properties" key.
|
||||
func Properties(props map[string]any) PropertyOption {
|
||||
return func(schema map[string]any) {
|
||||
schema["properties"] = props
|
||||
|
||||
527
vendor/github.com/mark3labs/mcp-go/mcp/types.go
generated
vendored
527
vendor/github.com/mark3labs/mcp-go/mcp/types.go
generated
vendored
@@ -6,9 +6,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strconv"
|
||||
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/yosida95/uritemplate/v3"
|
||||
)
|
||||
@@ -56,6 +55,33 @@ const (
|
||||
// https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/logging
|
||||
MethodSetLogLevel MCPMethod = "logging/setLevel"
|
||||
|
||||
// MethodElicitationCreate requests additional information from the user during interactions.
|
||||
// https://modelcontextprotocol.io/docs/concepts/elicitation
|
||||
MethodElicitationCreate MCPMethod = "elicitation/create"
|
||||
|
||||
// MethodNotificationElicitationComplete notifies when a URL mode elicitation completes.
|
||||
MethodNotificationElicitationComplete MCPMethod = "notifications/elicitation/complete"
|
||||
|
||||
// MethodListRoots requests roots list from the client during interactions.
|
||||
// https://modelcontextprotocol.io/specification/2025-06-18/client/roots
|
||||
MethodListRoots MCPMethod = "roots/list"
|
||||
|
||||
// MethodTasksGet retrieves the current status of a task.
|
||||
// https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks
|
||||
MethodTasksGet MCPMethod = "tasks/get"
|
||||
|
||||
// MethodTasksList lists all tasks for the current session.
|
||||
// https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks
|
||||
MethodTasksList MCPMethod = "tasks/list"
|
||||
|
||||
// MethodTasksResult retrieves the result of a completed task.
|
||||
// https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks
|
||||
MethodTasksResult MCPMethod = "tasks/result"
|
||||
|
||||
// MethodTasksCancel cancels an in-progress task.
|
||||
// https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks
|
||||
MethodTasksCancel MCPMethod = "tasks/cancel"
|
||||
|
||||
// MethodNotificationResourcesListChanged notifies when the list of available resources changes.
|
||||
// https://modelcontextprotocol.io/specification/2025-03-26/server/resources#list-changed-notification
|
||||
MethodNotificationResourcesListChanged = "notifications/resources/list_changed"
|
||||
@@ -67,8 +93,20 @@ const (
|
||||
MethodNotificationPromptsListChanged = "notifications/prompts/list_changed"
|
||||
|
||||
// MethodNotificationToolsListChanged notifies when the list of available tools changes.
|
||||
// https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/list_changed/
|
||||
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#list-changed-notification
|
||||
MethodNotificationToolsListChanged = "notifications/tools/list_changed"
|
||||
|
||||
// MethodNotificationRootsListChanged notifies when the list of available roots changes.
|
||||
// https://modelcontextprotocol.io/specification/2025-06-18/client/roots#root-list-changes
|
||||
MethodNotificationRootsListChanged = "notifications/roots/list_changed"
|
||||
|
||||
// MethodNotificationTasksStatus notifies when a task's status changes.
|
||||
// https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks
|
||||
MethodNotificationTasksStatus = "notifications/tasks/status"
|
||||
|
||||
// MethodCompletionComplete returns completion suggestions for a given argument
|
||||
// https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion
|
||||
MethodCompletionComplete MCPMethod = "completion/complete"
|
||||
)
|
||||
|
||||
type URITemplate struct {
|
||||
@@ -98,11 +136,12 @@ func (t *URITemplate) UnmarshalJSON(data []byte) error {
|
||||
type JSONRPCMessage any
|
||||
|
||||
// LATEST_PROTOCOL_VERSION is the most recent version of the MCP protocol.
|
||||
const LATEST_PROTOCOL_VERSION = "2025-06-18"
|
||||
const LATEST_PROTOCOL_VERSION = "2025-11-25"
|
||||
|
||||
// ValidProtocolVersions lists all known valid MCP protocol versions.
|
||||
var ValidProtocolVersions = []string{
|
||||
LATEST_PROTOCOL_VERSION,
|
||||
"2025-06-18",
|
||||
"2025-03-26",
|
||||
"2024-11-05",
|
||||
}
|
||||
@@ -294,7 +333,6 @@ func (r RequestId) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
func (r *RequestId) UnmarshalJSON(data []byte) error {
|
||||
|
||||
if string(data) == "null" {
|
||||
r.value = nil
|
||||
return nil
|
||||
@@ -344,32 +382,52 @@ type JSONRPCResponse struct {
|
||||
|
||||
// JSONRPCError represents a non-successful (error) response to a request.
|
||||
type JSONRPCError struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID RequestId `json:"id"`
|
||||
Error struct {
|
||||
// The error type that occurred.
|
||||
Code int `json:"code"`
|
||||
// A short description of the error. The message SHOULD be limited
|
||||
// to a concise single sentence.
|
||||
Message string `json:"message"`
|
||||
// Additional information about the error. The value of this member
|
||||
// is defined by the sender (e.g. detailed error information, nested errors etc.).
|
||||
Data any `json:"data,omitempty"`
|
||||
} `json:"error"`
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID RequestId `json:"id"`
|
||||
Error JSONRPCErrorDetails `json:"error"`
|
||||
}
|
||||
|
||||
// JSONRPCErrorDetails represents a JSON-RPC error for Go error handling.
|
||||
// This is separate from the JSONRPCError type which represents the full JSON-RPC error response structure.
|
||||
type JSONRPCErrorDetails struct {
|
||||
// The error type that occurred.
|
||||
Code int `json:"code"`
|
||||
// A short description of the error. The message SHOULD be limited
|
||||
// to a concise single sentence.
|
||||
Message string `json:"message"`
|
||||
// Additional information about the error. The value of this member
|
||||
// is defined by the sender (e.g. detailed error information, nested errors etc.).
|
||||
Data any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// Standard JSON-RPC error codes
|
||||
const (
|
||||
PARSE_ERROR = -32700
|
||||
INVALID_REQUEST = -32600
|
||||
// PARSE_ERROR indicates invalid JSON was received by the server.
|
||||
PARSE_ERROR = -32700
|
||||
|
||||
// INVALID_REQUEST indicates the JSON sent is not a valid Request object.
|
||||
INVALID_REQUEST = -32600
|
||||
|
||||
// METHOD_NOT_FOUND indicates the method does not exist/is not available.
|
||||
METHOD_NOT_FOUND = -32601
|
||||
INVALID_PARAMS = -32602
|
||||
INTERNAL_ERROR = -32603
|
||||
|
||||
// INVALID_PARAMS indicates invalid method parameter(s).
|
||||
INVALID_PARAMS = -32602
|
||||
|
||||
// INTERNAL_ERROR indicates internal JSON-RPC error.
|
||||
INTERNAL_ERROR = -32603
|
||||
|
||||
// REQUEST_INTERRUPTED indicates a request was cancelled or timed out.
|
||||
REQUEST_INTERRUPTED = -32800
|
||||
)
|
||||
|
||||
// MCP error codes
|
||||
const (
|
||||
// RESOURCE_NOT_FOUND indicates that the requested resource was not found.
|
||||
RESOURCE_NOT_FOUND = -32002
|
||||
|
||||
// URL_ELICITATION_REQUIRED is the error code for when URL elicitation is required.
|
||||
URL_ELICITATION_REQUIRED = -32042
|
||||
)
|
||||
|
||||
/* Empty result */
|
||||
@@ -453,6 +511,8 @@ type InitializedNotification struct {
|
||||
// capabilities are defined here, in this schema, but this is not a closed set: any
|
||||
// client can define its own, additional capabilities.
|
||||
type ClientCapabilities struct {
|
||||
// Optional, present if the client is advertising extension support.
|
||||
Extensions map[string]any `json:"extensions,omitempty"`
|
||||
// Experimental, non-standard capabilities that the client supports.
|
||||
Experimental map[string]any `json:"experimental,omitempty"`
|
||||
// Present if the client supports listing roots.
|
||||
@@ -462,12 +522,18 @@ type ClientCapabilities struct {
|
||||
} `json:"roots,omitempty"`
|
||||
// Present if the client supports sampling from an LLM.
|
||||
Sampling *struct{} `json:"sampling,omitempty"`
|
||||
// Present if the client supports elicitation requests from the server.
|
||||
Elicitation *ElicitationCapability `json:"elicitation,omitempty"`
|
||||
// Present if the client supports task-based execution.
|
||||
Tasks *TasksCapability `json:"tasks,omitempty"`
|
||||
}
|
||||
|
||||
// ServerCapabilities represents capabilities that a server may support. Known
|
||||
// capabilities are defined here, in this schema, but this is not a closed set: any
|
||||
// server can define its own, additional capabilities.
|
||||
type ServerCapabilities struct {
|
||||
// Optional, present if the server is advertising extension support.
|
||||
Extensions map[string]any `json:"extensions,omitempty"`
|
||||
// Experimental, non-standard capabilities that the server supports.
|
||||
Experimental map[string]any `json:"experimental,omitempty"`
|
||||
// Present if the server supports sending log messages to the client.
|
||||
@@ -492,12 +558,44 @@ type ServerCapabilities struct {
|
||||
// Whether this server supports notifications for changes to the tool list.
|
||||
ListChanged bool `json:"listChanged,omitempty"`
|
||||
} `json:"tools,omitempty"`
|
||||
// Present if the server supports elicitation requests to the client.
|
||||
Elicitation *ElicitationCapability `json:"elicitation,omitempty"`
|
||||
// Present if the server supports roots requests to the client.
|
||||
Roots *struct{} `json:"roots,omitempty"`
|
||||
// Present if the server supports task-based execution.
|
||||
Tasks *TasksCapability `json:"tasks,omitempty"`
|
||||
// Present if the server supports completions requests to the client.
|
||||
Completions *struct{} `json:"completions,omitempty"`
|
||||
}
|
||||
|
||||
// Icon represents a visual identifier for MCP entities.
|
||||
//
|
||||
// Security considerations:
|
||||
// - Clients MUST support at least image/png and image/jpeg MIME types
|
||||
// - Clients SHOULD support image/svg+xml and image/webp
|
||||
// - Icons should be treated as untrusted input
|
||||
// - URI scheme validation (HTTPS or data URI only)
|
||||
// - Size/dimension limits to prevent resource exhaustion
|
||||
type Icon struct {
|
||||
// URI pointing to the icon resource (HTTPS URL or data URI)
|
||||
Src string `json:"src"`
|
||||
|
||||
// Optional MIME type (e.g., "image/png", "image/svg+xml")
|
||||
MIMEType string `json:"mimeType,omitempty"`
|
||||
|
||||
// Optional size specifications (e.g., ["48x48"], ["any"] for SVG)
|
||||
Sizes []string `json:"sizes,omitempty"`
|
||||
}
|
||||
|
||||
// Implementation describes the name and version of an MCP implementation.
|
||||
type Implementation struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
WebsiteURL string `json:"websiteUrl,omitempty"`
|
||||
// Icons provides visual identifiers for the implementation
|
||||
Icons []Icon `json:"icons,omitempty"`
|
||||
}
|
||||
|
||||
/* Ping */
|
||||
@@ -674,6 +772,8 @@ type Resource struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
// The MIME type of this resource, if known.
|
||||
MIMEType string `json:"mimeType,omitempty"`
|
||||
// Icons provides visual identifiers for the resource
|
||||
Icons []Icon `json:"icons,omitempty"`
|
||||
}
|
||||
|
||||
// GetName returns the name of the resource.
|
||||
@@ -702,6 +802,8 @@ type ResourceTemplate struct {
|
||||
// The MIME type for all resources that match this template. This should only
|
||||
// be included if all resources matching this template have the same type.
|
||||
MIMEType string `json:"mimeType,omitempty"`
|
||||
// Icons provides visual identifiers for the resource template
|
||||
Icons []Icon `json:"icons,omitempty"`
|
||||
}
|
||||
|
||||
// GetName returns the name of the resourceTemplate.
|
||||
@@ -716,8 +818,9 @@ type ResourceContents interface {
|
||||
}
|
||||
|
||||
type TextResourceContents struct {
|
||||
// Meta is a metadata object that is reserved by MCP for storing additional information.
|
||||
Meta *Meta `json:"_meta,omitempty"`
|
||||
// Raw per‑resource metadata; pass‑through as defined by MCP. Not the same as mcp.Meta.
|
||||
// Allows _meta to be used for MCP-UI features for example. Does not assume any specific format.
|
||||
Meta map[string]any `json:"_meta,omitempty"`
|
||||
// The URI of this resource.
|
||||
URI string `json:"uri"`
|
||||
// The MIME type of this resource, if known.
|
||||
@@ -730,8 +833,9 @@ type TextResourceContents struct {
|
||||
func (TextResourceContents) isResourceContents() {}
|
||||
|
||||
type BlobResourceContents struct {
|
||||
// Meta is a metadata object that is reserved by MCP for storing additional information.
|
||||
Meta *Meta `json:"_meta,omitempty"`
|
||||
// Raw per‑resource metadata; pass‑through as defined by MCP. Not the same as mcp.Meta.
|
||||
// Allows _meta to be used for MCP-UI features for example. Does not assume any specific format.
|
||||
Meta map[string]any `json:"_meta,omitempty"`
|
||||
// The URI of this resource.
|
||||
URI string `json:"uri"`
|
||||
// The MIME type of this resource, if known.
|
||||
@@ -814,6 +918,89 @@ func (l LoggingLevel) ShouldSendTo(minLevel LoggingLevel) bool {
|
||||
return ia >= ib
|
||||
}
|
||||
|
||||
/* Elicitation */
|
||||
|
||||
// ElicitationRequest is a request from the server to the client to request additional
|
||||
// information from the user during an interaction.
|
||||
type ElicitationRequest struct {
|
||||
Request
|
||||
Params ElicitationParams `json:"params"`
|
||||
}
|
||||
|
||||
// ElicitationParams contains the parameters for an elicitation request.
|
||||
type ElicitationParams struct {
|
||||
Meta *Meta `json:"_meta,omitempty"`
|
||||
// Mode specifies the type of elicitation: "form" or "url". Defaults to "form".
|
||||
Mode string `json:"mode,omitempty"`
|
||||
// A human-readable message explaining what information is being requested and why.
|
||||
Message string `json:"message"`
|
||||
|
||||
// Form mode fields
|
||||
|
||||
// A JSON Schema defining the expected structure of the user's response.
|
||||
RequestedSchema any `json:"requestedSchema,omitempty"`
|
||||
|
||||
// URL mode fields
|
||||
|
||||
// ElicitationID is a unique identifier for the elicitation request.
|
||||
ElicitationID string `json:"elicitationId,omitempty"`
|
||||
// URL is the URL to be opened by the user.
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// Validate checks if the elicitation parameters are valid.
|
||||
func (p ElicitationParams) Validate() error {
|
||||
mode := p.Mode
|
||||
if mode == "" {
|
||||
mode = ElicitationModeForm
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case ElicitationModeForm:
|
||||
if p.RequestedSchema == nil {
|
||||
return fmt.Errorf("requestedSchema is required for form elicitation")
|
||||
}
|
||||
case ElicitationModeURL:
|
||||
if p.ElicitationID == "" {
|
||||
return fmt.Errorf("elicitationId is required for url elicitation")
|
||||
}
|
||||
if p.URL == "" {
|
||||
return fmt.Errorf("url is required for url elicitation")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid elicitation mode: %s", mode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ElicitationResult represents the result of an elicitation request.
|
||||
type ElicitationResult struct {
|
||||
Result
|
||||
ElicitationResponse
|
||||
}
|
||||
|
||||
// ElicitationResponse represents the user's response to an elicitation request.
|
||||
type ElicitationResponse struct {
|
||||
// Action indicates whether the user accepted, declined, or cancelled.
|
||||
Action ElicitationResponseAction `json:"action"`
|
||||
// Content contains the user's response data if they accepted.
|
||||
// Should conform to the requestedSchema from the ElicitationRequest.
|
||||
Content any `json:"content,omitempty"`
|
||||
}
|
||||
|
||||
// ElicitationResponseAction indicates how the user responded to an elicitation request.
|
||||
type ElicitationResponseAction string
|
||||
|
||||
const (
|
||||
// ElicitationResponseActionAccept indicates the user provided the requested information.
|
||||
ElicitationResponseActionAccept ElicitationResponseAction = "accept"
|
||||
// ElicitationResponseActionDecline indicates the user explicitly declined to provide information.
|
||||
ElicitationResponseActionDecline ElicitationResponseAction = "decline"
|
||||
// ElicitationResponseActionCancel indicates the user cancelled without making a choice.
|
||||
ElicitationResponseActionCancel ElicitationResponseAction = "cancel"
|
||||
)
|
||||
|
||||
/* Sampling */
|
||||
|
||||
const (
|
||||
@@ -872,7 +1059,10 @@ type Annotations struct {
|
||||
// A value of 1 means "most important," and indicates that the data is
|
||||
// effectively required, while 0 means "least important," and indicates that
|
||||
// the data is entirely optional.
|
||||
Priority float64 `json:"priority,omitempty"`
|
||||
// Priority ranges from 0.0 to 1.0 (1 = most important, 0 = least important).
|
||||
Priority *float64 `json:"priority,omitempty"`
|
||||
// ISO 8601 formatted timestamp (e.g., "2025-01-12T15:00:58Z")
|
||||
LastModified string `json:"lastModified,omitempty"`
|
||||
}
|
||||
|
||||
// Annotated is the base for objects that include optional annotations for the
|
||||
@@ -1024,29 +1214,81 @@ type CompleteRequest struct {
|
||||
Header http.Header `json:"-"`
|
||||
}
|
||||
|
||||
// CompleteParams are the parameters for a completion/complete request
|
||||
type CompleteParams struct {
|
||||
Ref any `json:"ref"` // Can be PromptReference or ResourceReference
|
||||
Argument struct {
|
||||
// The name of the argument
|
||||
Name string `json:"name"`
|
||||
// The value of the argument to use for completion matching.
|
||||
Value string `json:"value"`
|
||||
} `json:"argument"`
|
||||
Ref any `json:"ref"` // Can be PromptReference or ResourceReference
|
||||
Argument CompleteArgument `json:"argument"`
|
||||
Context CompleteContext `json:"context"`
|
||||
}
|
||||
|
||||
func (p *CompleteParams) UnmarshalJSON(data []byte) error {
|
||||
// Use a temporary type to avoid infinite recursion on UnmarshalJSON
|
||||
type Alias CompleteParams
|
||||
aux := &struct {
|
||||
// Use RawMessage to delay unmarshalling until after the type is known
|
||||
Ref json.RawMessage `json:"ref"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(p),
|
||||
}
|
||||
if err := json.Unmarshal(data, aux); err != nil {
|
||||
return err
|
||||
}
|
||||
// Use a temporary "type peek" struct to determine the type
|
||||
var typePeek struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
if err := json.Unmarshal(aux.Ref, &typePeek); err != nil {
|
||||
return err
|
||||
}
|
||||
switch typePeek.Type {
|
||||
case "ref/prompt":
|
||||
var prompt PromptReference
|
||||
if err := json.Unmarshal(aux.Ref, &prompt); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Ref = prompt
|
||||
case "ref/resource":
|
||||
var resource ResourceReference
|
||||
if err := json.Unmarshal(aux.Ref, &resource); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Ref = resource
|
||||
default:
|
||||
return fmt.Errorf("unknown reference type: %s", typePeek.Type)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompleteResult is the server's response to a completion/complete request
|
||||
type CompleteResult struct {
|
||||
Result
|
||||
Completion struct {
|
||||
// An array of completion values. Must not exceed 100 items.
|
||||
Values []string `json:"values"`
|
||||
// The total number of completion options available. This can exceed the
|
||||
// number of values actually sent in the response.
|
||||
Total int `json:"total,omitempty"`
|
||||
// Indicates whether there are additional completion options beyond those
|
||||
// provided in the current response, even if the exact total is unknown.
|
||||
HasMore bool `json:"hasMore,omitempty"`
|
||||
} `json:"completion"`
|
||||
Completion Completion `json:"completion"`
|
||||
}
|
||||
|
||||
// CompleteArgument is an argument to a completion request
|
||||
type CompleteArgument struct {
|
||||
// The name of the argument
|
||||
Name string `json:"name"`
|
||||
// The value of the argument to use for completion matching.
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// CompleteContext is the context about already-resolved arguments
|
||||
type CompleteContext struct {
|
||||
Arguments map[string]string `json:"arguments"`
|
||||
}
|
||||
|
||||
// Completion is the server's response to a completion/complete request
|
||||
type Completion struct {
|
||||
// An array of completion values. Must not exceed 100 items.
|
||||
Values []string `json:"values"`
|
||||
// The total number of completion options available. This can exceed the
|
||||
// number of values actually sent in the response.
|
||||
Total int `json:"total,omitempty"`
|
||||
// Indicates whether there are additional completion options beyond those
|
||||
// provided in the current response, even if the exact total is unknown.
|
||||
HasMore bool `json:"hasMore,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceReference is a reference to a resource or resource template definition.
|
||||
@@ -1074,7 +1316,6 @@ type PromptReference struct {
|
||||
// structure or access specific locations that the client has permission to read from.
|
||||
type ListRootsRequest struct {
|
||||
Request
|
||||
Header http.Header `json:"-"`
|
||||
}
|
||||
|
||||
// ListRootsResult is the client's response to a roots/list request from the server.
|
||||
@@ -1107,6 +1348,179 @@ type RootsListChangedNotification struct {
|
||||
Notification
|
||||
}
|
||||
|
||||
/* Tasks */
|
||||
|
||||
// TasksCapability represents the task capabilities that a client or server may support.
|
||||
// Tasks enable long-running, asynchronous operations with status polling.
|
||||
type TasksCapability struct {
|
||||
// Whether the party supports the tasks/list operation.
|
||||
List *struct{} `json:"list,omitempty"`
|
||||
// Whether the party supports the tasks/cancel operation.
|
||||
Cancel *struct{} `json:"cancel,omitempty"`
|
||||
// Requests that can be augmented with task metadata.
|
||||
Requests *TaskRequestsCapability `json:"requests,omitempty"`
|
||||
}
|
||||
|
||||
// TaskRequestsCapability indicates which request types support task augmentation.
|
||||
type TaskRequestsCapability struct {
|
||||
// Tool-related capabilities.
|
||||
Tools *struct {
|
||||
// Whether tools/call can be augmented with task metadata.
|
||||
Call *struct{} `json:"call,omitempty"`
|
||||
} `json:"tools,omitempty"`
|
||||
// Sampling-related capabilities.
|
||||
Sampling *struct {
|
||||
// Whether sampling/createMessage can be augmented with task metadata.
|
||||
CreateMessage *struct{} `json:"createMessage,omitempty"`
|
||||
} `json:"sampling,omitempty"`
|
||||
// Elicitation-related capabilities.
|
||||
Elicitation *struct {
|
||||
// Whether elicitation/create can be augmented with task metadata.
|
||||
Create *struct{} `json:"create,omitempty"`
|
||||
} `json:"elicitation,omitempty"`
|
||||
}
|
||||
|
||||
// TaskStatus represents the execution state of a task.
|
||||
type TaskStatus string
|
||||
|
||||
const (
|
||||
// TaskStatusWorking indicates the request is currently being processed.
|
||||
TaskStatusWorking TaskStatus = "working"
|
||||
// TaskStatusInputRequired indicates the receiver needs input from the requestor.
|
||||
// NOTE: This status is defined by the spec but not yet implemented in this SDK.
|
||||
// The input_required flow requires integration with elicitation which is planned
|
||||
// for a future release.
|
||||
TaskStatusInputRequired TaskStatus = "input_required"
|
||||
// TaskStatusCompleted indicates the request completed successfully.
|
||||
TaskStatusCompleted TaskStatus = "completed"
|
||||
// TaskStatusFailed indicates the request did not complete successfully.
|
||||
TaskStatusFailed TaskStatus = "failed"
|
||||
// TaskStatusCancelled indicates the request was cancelled before completion.
|
||||
TaskStatusCancelled TaskStatus = "cancelled"
|
||||
)
|
||||
|
||||
// IsTerminal returns true if the task status is terminal (completed, failed, or cancelled).
|
||||
func (s TaskStatus) IsTerminal() bool {
|
||||
return s == TaskStatusCompleted || s == TaskStatusFailed || s == TaskStatusCancelled
|
||||
}
|
||||
|
||||
// Task represents the execution state of a request.
|
||||
type Task struct {
|
||||
// Unique identifier for the task.
|
||||
TaskId string `json:"taskId"`
|
||||
// Current state of the task execution.
|
||||
Status TaskStatus `json:"status"`
|
||||
// Optional human-readable message describing the current state.
|
||||
StatusMessage string `json:"statusMessage,omitempty"`
|
||||
// ISO 8601 timestamp when the task was created.
|
||||
CreatedAt string `json:"createdAt"`
|
||||
// ISO 8601 timestamp when the task was last updated.
|
||||
LastUpdatedAt string `json:"lastUpdatedAt"`
|
||||
// Time in milliseconds from creation before task may be deleted.
|
||||
// If null, the task has no expiration.
|
||||
TTL *int64 `json:"ttl"`
|
||||
// Suggested time in milliseconds between status checks.
|
||||
PollInterval *int64 `json:"pollInterval,omitempty"`
|
||||
}
|
||||
|
||||
// GetName returns the task ID, implementing the Named interface for pagination.
|
||||
func (t Task) GetName() string {
|
||||
return t.TaskId
|
||||
}
|
||||
|
||||
// TaskParams represents the task metadata included when augmenting a request.
|
||||
type TaskParams struct {
|
||||
// Requested duration in milliseconds to retain task from creation.
|
||||
TTL *int64 `json:"ttl,omitempty"`
|
||||
}
|
||||
|
||||
// CreateTaskResult is returned immediately when a task-augmented request is accepted.
|
||||
// It contains task metadata rather than the actual operation result.
|
||||
type CreateTaskResult struct {
|
||||
Result
|
||||
Task Task `json:"task"`
|
||||
Content []Content `json:"-"`
|
||||
StructuredContent any `json:"-"`
|
||||
IsError bool `json:"-"`
|
||||
}
|
||||
|
||||
// GetTaskRequest retrieves the current status of a task.
|
||||
type GetTaskRequest struct {
|
||||
Request
|
||||
Header http.Header `json:"-"`
|
||||
Params GetTaskParams `json:"params"`
|
||||
}
|
||||
|
||||
type GetTaskParams struct {
|
||||
TaskId string `json:"taskId"`
|
||||
}
|
||||
|
||||
// GetTaskResult returns the current state of a task.
|
||||
type GetTaskResult struct {
|
||||
Result
|
||||
Task
|
||||
}
|
||||
|
||||
// ListTasksRequest retrieves a paginated list of tasks.
|
||||
type ListTasksRequest struct {
|
||||
PaginatedRequest
|
||||
Header http.Header `json:"-"`
|
||||
}
|
||||
|
||||
// ListTasksResult returns a list of tasks.
|
||||
type ListTasksResult struct {
|
||||
PaginatedResult
|
||||
Tasks []Task `json:"tasks"`
|
||||
}
|
||||
|
||||
// TaskResultRequest retrieves the result of a completed task.
|
||||
type TaskResultRequest struct {
|
||||
Request
|
||||
Header http.Header `json:"-"`
|
||||
Params TaskResultParams `json:"params"`
|
||||
}
|
||||
|
||||
type TaskResultParams struct {
|
||||
TaskId string `json:"taskId"`
|
||||
}
|
||||
|
||||
// TaskResultResult contains the actual operation result.
|
||||
// For task-augmented tool calls, this embeds the CallToolResult fields.
|
||||
type TaskResultResult struct {
|
||||
Result
|
||||
// Tool call result fields (for task-augmented tool calls)
|
||||
Content []Content `json:"content,omitempty"`
|
||||
StructuredContent any `json:"structuredContent,omitempty"`
|
||||
IsError bool `json:"isError,omitempty"`
|
||||
}
|
||||
|
||||
// CancelTaskRequest cancels an in-progress task.
|
||||
type CancelTaskRequest struct {
|
||||
Request
|
||||
Header http.Header `json:"-"`
|
||||
Params CancelTaskParams `json:"params"`
|
||||
}
|
||||
|
||||
type CancelTaskParams struct {
|
||||
TaskId string `json:"taskId"`
|
||||
}
|
||||
|
||||
// CancelTaskResult returns the cancelled task state.
|
||||
type CancelTaskResult struct {
|
||||
Result
|
||||
Task
|
||||
}
|
||||
|
||||
// TaskStatusNotification is sent when a task's status changes.
|
||||
type TaskStatusNotification struct {
|
||||
Notification
|
||||
Params TaskStatusNotificationParams `json:"params"`
|
||||
}
|
||||
|
||||
type TaskStatusNotificationParams struct {
|
||||
Task
|
||||
}
|
||||
|
||||
// ClientRequest represents any request that can be sent from client to server.
|
||||
type ClientRequest any
|
||||
|
||||
@@ -1171,3 +1585,24 @@ func UnmarshalContent(data []byte) (Content, error) {
|
||||
return nil, fmt.Errorf("unknown content type: %s", contentType)
|
||||
}
|
||||
}
|
||||
|
||||
// ElicitationCapability represents the elicitation capabilities of a client or server.
|
||||
type ElicitationCapability struct {
|
||||
Form *struct{} `json:"form,omitempty"` // Supports form mode
|
||||
URL *struct{} `json:"url,omitempty"` // Supports URL mode
|
||||
}
|
||||
|
||||
// NewElicitationCompleteNotification creates a new elicitation complete notification.
|
||||
func NewElicitationCompleteNotification(elicitationID string) JSONRPCNotification {
|
||||
return JSONRPCNotification{
|
||||
JSONRPC: JSONRPC_VERSION,
|
||||
Notification: Notification{
|
||||
Method: string(MethodNotificationElicitationComplete),
|
||||
Params: NotificationParams{
|
||||
AdditionalFields: map[string]any{
|
||||
"elicitationId": elicitationID,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
446
vendor/github.com/mark3labs/mcp-go/mcp/utils.go
generated
vendored
446
vendor/github.com/mark3labs/mcp-go/mcp/utils.go
generated
vendored
@@ -8,54 +8,66 @@ import (
|
||||
)
|
||||
|
||||
// ClientRequest types
|
||||
var _ ClientRequest = &PingRequest{}
|
||||
var _ ClientRequest = &InitializeRequest{}
|
||||
var _ ClientRequest = &CompleteRequest{}
|
||||
var _ ClientRequest = &SetLevelRequest{}
|
||||
var _ ClientRequest = &GetPromptRequest{}
|
||||
var _ ClientRequest = &ListPromptsRequest{}
|
||||
var _ ClientRequest = &ListResourcesRequest{}
|
||||
var _ ClientRequest = &ReadResourceRequest{}
|
||||
var _ ClientRequest = &SubscribeRequest{}
|
||||
var _ ClientRequest = &UnsubscribeRequest{}
|
||||
var _ ClientRequest = &CallToolRequest{}
|
||||
var _ ClientRequest = &ListToolsRequest{}
|
||||
var (
|
||||
_ ClientRequest = (*PingRequest)(nil)
|
||||
_ ClientRequest = (*InitializeRequest)(nil)
|
||||
_ ClientRequest = (*CompleteRequest)(nil)
|
||||
_ ClientRequest = (*SetLevelRequest)(nil)
|
||||
_ ClientRequest = (*GetPromptRequest)(nil)
|
||||
_ ClientRequest = (*ListPromptsRequest)(nil)
|
||||
_ ClientRequest = (*ListResourcesRequest)(nil)
|
||||
_ ClientRequest = (*ReadResourceRequest)(nil)
|
||||
_ ClientRequest = (*SubscribeRequest)(nil)
|
||||
_ ClientRequest = (*UnsubscribeRequest)(nil)
|
||||
_ ClientRequest = (*CallToolRequest)(nil)
|
||||
_ ClientRequest = (*ListToolsRequest)(nil)
|
||||
)
|
||||
|
||||
// ClientNotification types
|
||||
var _ ClientNotification = &CancelledNotification{}
|
||||
var _ ClientNotification = &ProgressNotification{}
|
||||
var _ ClientNotification = &InitializedNotification{}
|
||||
var _ ClientNotification = &RootsListChangedNotification{}
|
||||
var (
|
||||
_ ClientNotification = (*CancelledNotification)(nil)
|
||||
_ ClientNotification = (*ProgressNotification)(nil)
|
||||
_ ClientNotification = (*InitializedNotification)(nil)
|
||||
_ ClientNotification = (*RootsListChangedNotification)(nil)
|
||||
)
|
||||
|
||||
// ClientResult types
|
||||
var _ ClientResult = &EmptyResult{}
|
||||
var _ ClientResult = &CreateMessageResult{}
|
||||
var _ ClientResult = &ListRootsResult{}
|
||||
var (
|
||||
_ ClientResult = (*EmptyResult)(nil)
|
||||
_ ClientResult = (*CreateMessageResult)(nil)
|
||||
_ ClientResult = (*ListRootsResult)(nil)
|
||||
)
|
||||
|
||||
// ServerRequest types
|
||||
var _ ServerRequest = &PingRequest{}
|
||||
var _ ServerRequest = &CreateMessageRequest{}
|
||||
var _ ServerRequest = &ListRootsRequest{}
|
||||
var (
|
||||
_ ServerRequest = (*PingRequest)(nil)
|
||||
_ ServerRequest = (*CreateMessageRequest)(nil)
|
||||
_ ServerRequest = (*ListRootsRequest)(nil)
|
||||
)
|
||||
|
||||
// ServerNotification types
|
||||
var _ ServerNotification = &CancelledNotification{}
|
||||
var _ ServerNotification = &ProgressNotification{}
|
||||
var _ ServerNotification = &LoggingMessageNotification{}
|
||||
var _ ServerNotification = &ResourceUpdatedNotification{}
|
||||
var _ ServerNotification = &ResourceListChangedNotification{}
|
||||
var _ ServerNotification = &ToolListChangedNotification{}
|
||||
var _ ServerNotification = &PromptListChangedNotification{}
|
||||
var (
|
||||
_ ServerNotification = (*CancelledNotification)(nil)
|
||||
_ ServerNotification = (*ProgressNotification)(nil)
|
||||
_ ServerNotification = (*LoggingMessageNotification)(nil)
|
||||
_ ServerNotification = (*ResourceUpdatedNotification)(nil)
|
||||
_ ServerNotification = (*ResourceListChangedNotification)(nil)
|
||||
_ ServerNotification = (*ToolListChangedNotification)(nil)
|
||||
_ ServerNotification = (*PromptListChangedNotification)(nil)
|
||||
)
|
||||
|
||||
// ServerResult types
|
||||
var _ ServerResult = &EmptyResult{}
|
||||
var _ ServerResult = &InitializeResult{}
|
||||
var _ ServerResult = &CompleteResult{}
|
||||
var _ ServerResult = &GetPromptResult{}
|
||||
var _ ServerResult = &ListPromptsResult{}
|
||||
var _ ServerResult = &ListResourcesResult{}
|
||||
var _ ServerResult = &ReadResourceResult{}
|
||||
var _ ServerResult = &CallToolResult{}
|
||||
var _ ServerResult = &ListToolsResult{}
|
||||
var (
|
||||
_ ServerResult = (*EmptyResult)(nil)
|
||||
_ ServerResult = (*InitializeResult)(nil)
|
||||
_ ServerResult = (*CompleteResult)(nil)
|
||||
_ ServerResult = (*GetPromptResult)(nil)
|
||||
_ ServerResult = (*ListPromptsResult)(nil)
|
||||
_ ServerResult = (*ListResourcesResult)(nil)
|
||||
_ ServerResult = (*ReadResourceResult)(nil)
|
||||
_ ServerResult = (*CallToolResult)(nil)
|
||||
_ ServerResult = (*ListToolsResult)(nil)
|
||||
)
|
||||
|
||||
// Helper functions for type assertions
|
||||
|
||||
@@ -100,7 +112,10 @@ func AsBlobResourceContents(content any) (*BlobResourceContents, bool) {
|
||||
|
||||
// Helper function for JSON-RPC
|
||||
|
||||
// NewJSONRPCResponse creates a new JSONRPCResponse with the given id and result
|
||||
// NewJSONRPCResponse creates a new JSONRPCResponse with the given id and result.
|
||||
// NOTE: This function expects a Result struct, but JSONRPCResponse.Result is typed as `any`.
|
||||
// The Result struct wraps the actual result data with optional metadata.
|
||||
// For direct result assignment, use NewJSONRPCResultResponse instead.
|
||||
func NewJSONRPCResponse(id RequestId, result Result) JSONRPCResponse {
|
||||
return JSONRPCResponse{
|
||||
JSONRPC: JSONRPC_VERSION,
|
||||
@@ -109,6 +124,25 @@ func NewJSONRPCResponse(id RequestId, result Result) JSONRPCResponse {
|
||||
}
|
||||
}
|
||||
|
||||
// NewJSONRPCResultResponse creates a new JSONRPCResponse with the given id and result.
|
||||
// This function accepts any type for the result, matching the JSONRPCResponse.Result field type.
|
||||
func NewJSONRPCResultResponse(id RequestId, result any) JSONRPCResponse {
|
||||
return JSONRPCResponse{
|
||||
JSONRPC: JSONRPC_VERSION,
|
||||
ID: id,
|
||||
Result: result,
|
||||
}
|
||||
}
|
||||
|
||||
// NewJSONRPCErrorDetails creates a new JSONRPCErrorDetails with the given code, message, and data.
|
||||
func NewJSONRPCErrorDetails(code int, message string, data any) JSONRPCErrorDetails {
|
||||
return JSONRPCErrorDetails{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// NewJSONRPCError creates a new JSONRPCResponse with the given id, code, and message
|
||||
func NewJSONRPCError(
|
||||
id RequestId,
|
||||
@@ -119,15 +153,7 @@ func NewJSONRPCError(
|
||||
return JSONRPCError{
|
||||
JSONRPC: JSONRPC_VERSION,
|
||||
ID: id,
|
||||
Error: struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data,omitempty"`
|
||||
}{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
},
|
||||
Error: NewJSONRPCErrorDetails(code, message, data),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,6 +279,24 @@ func NewToolResultText(text string) *CallToolResult {
|
||||
}
|
||||
}
|
||||
|
||||
// NewToolResultJSON creates a new CallToolResult with a JSON content.
|
||||
func NewToolResultJSON[T any](data T) (*CallToolResult, error) {
|
||||
b, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to marshal JSON: %w", err)
|
||||
}
|
||||
|
||||
return &CallToolResult{
|
||||
Content: []Content{
|
||||
TextContent{
|
||||
Type: ContentTypeText,
|
||||
Text: string(b),
|
||||
},
|
||||
},
|
||||
StructuredContent: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewToolResultStructured creates a new CallToolResult with structured content.
|
||||
// It includes both the structured content and a text representation for backward compatibility.
|
||||
func NewToolResultStructured(structured any, fallbackText string) *CallToolResult {
|
||||
@@ -309,7 +353,7 @@ func NewToolResultImage(text, imageData, mimeType string) *CallToolResult {
|
||||
}
|
||||
|
||||
// NewToolResultAudio creates a new CallToolResult with both text and audio content
|
||||
func NewToolResultAudio(text, imageData, mimeType string) *CallToolResult {
|
||||
func NewToolResultAudio(text, audioData, mimeType string) *CallToolResult {
|
||||
return &CallToolResult{
|
||||
Content: []Content{
|
||||
TextContent{
|
||||
@@ -318,7 +362,7 @@ func NewToolResultAudio(text, imageData, mimeType string) *CallToolResult {
|
||||
},
|
||||
AudioContent{
|
||||
Type: ContentTypeAudio,
|
||||
Data: imageData,
|
||||
Data: audioData,
|
||||
MIMEType: mimeType,
|
||||
},
|
||||
},
|
||||
@@ -492,6 +536,42 @@ func ExtractString(data map[string]any, key string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ParseAnnotations parses priority, audience, and lastModified fields from the provided map
|
||||
// and returns an Annotations struct populated with any valid values found.
|
||||
// If data is nil, ParseAnnotations returns nil. Priority is set when a numeric value can be
|
||||
// parsed and is stored as a *float64. Audience is populated from string values and includes
|
||||
// only RoleUser and RoleAssistant entries. LastModified is set when the value is a string.
|
||||
func ParseAnnotations(data map[string]any) *Annotations {
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
annotations := &Annotations{}
|
||||
if value, ok := data["priority"]; ok {
|
||||
if value != nil {
|
||||
if priority, err := cast.ToFloat64E(value); err == nil {
|
||||
annotations.Priority = &priority
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if value, ok := data["audience"]; ok {
|
||||
for _, a := range cast.ToStringSlice(value) {
|
||||
a := Role(a)
|
||||
if a == RoleUser || a == RoleAssistant {
|
||||
annotations.Audience = append(annotations.Audience, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if value, ok := data["lastModified"]; ok {
|
||||
if str, ok := value.(string); ok {
|
||||
annotations.LastModified = str
|
||||
}
|
||||
}
|
||||
return annotations
|
||||
|
||||
}
|
||||
|
||||
func ExtractMap(data map[string]any, key string) map[string]any {
|
||||
if value, ok := data[key]; ok {
|
||||
if m, ok := value.(map[string]any); ok {
|
||||
@@ -501,13 +581,29 @@ func ExtractMap(data map[string]any, key string) map[string]any {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseContent parses a generic map into a strongly-typed Content value.
|
||||
// It extracts annotations and _meta fields from the map and sets them on
|
||||
// the returned content type.
|
||||
func ParseContent(contentMap map[string]any) (Content, error) {
|
||||
contentType := ExtractString(contentMap, "type")
|
||||
|
||||
var annotations *Annotations
|
||||
if annotationsMap := ExtractMap(contentMap, "annotations"); annotationsMap != nil {
|
||||
annotations = ParseAnnotations(annotationsMap)
|
||||
}
|
||||
|
||||
var meta *Meta
|
||||
if metaMap := ExtractMap(contentMap, "_meta"); metaMap != nil {
|
||||
meta = NewMetaFromMap(metaMap)
|
||||
}
|
||||
|
||||
switch contentType {
|
||||
case ContentTypeText:
|
||||
text := ExtractString(contentMap, "text")
|
||||
return NewTextContent(text), nil
|
||||
c := NewTextContent(text)
|
||||
c.Annotations = annotations
|
||||
c.Meta = meta
|
||||
return c, nil
|
||||
|
||||
case ContentTypeImage:
|
||||
data := ExtractString(contentMap, "data")
|
||||
@@ -515,7 +611,10 @@ func ParseContent(contentMap map[string]any) (Content, error) {
|
||||
if data == "" || mimeType == "" {
|
||||
return nil, fmt.Errorf("image data or mimeType is missing")
|
||||
}
|
||||
return NewImageContent(data, mimeType), nil
|
||||
c := NewImageContent(data, mimeType)
|
||||
c.Annotations = annotations
|
||||
c.Meta = meta
|
||||
return c, nil
|
||||
|
||||
case ContentTypeAudio:
|
||||
data := ExtractString(contentMap, "data")
|
||||
@@ -523,7 +622,10 @@ func ParseContent(contentMap map[string]any) (Content, error) {
|
||||
if data == "" || mimeType == "" {
|
||||
return nil, fmt.Errorf("audio data or mimeType is missing")
|
||||
}
|
||||
return NewAudioContent(data, mimeType), nil
|
||||
c := NewAudioContent(data, mimeType)
|
||||
c.Annotations = annotations
|
||||
c.Meta = meta
|
||||
return c, nil
|
||||
|
||||
case ContentTypeLink:
|
||||
uri := ExtractString(contentMap, "uri")
|
||||
@@ -533,7 +635,9 @@ func ParseContent(contentMap map[string]any) (Content, error) {
|
||||
if uri == "" || name == "" {
|
||||
return nil, fmt.Errorf("resource_link uri or name is missing")
|
||||
}
|
||||
return NewResourceLink(uri, name, description, mimeType), nil
|
||||
c := NewResourceLink(uri, name, description, mimeType)
|
||||
c.Annotations = annotations
|
||||
return c, nil
|
||||
|
||||
case ContentTypeResource:
|
||||
resourceMap := ExtractMap(contentMap, "resource")
|
||||
@@ -546,7 +650,10 @@ func ParseContent(contentMap map[string]any) (Content, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewEmbeddedResource(resourceContents), nil
|
||||
c := NewEmbeddedResource(resourceContents)
|
||||
c.Annotations = annotations
|
||||
c.Meta = meta
|
||||
return c, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported content type: %s", contentType)
|
||||
@@ -687,8 +794,15 @@ func ParseResourceContents(contentMap map[string]any) (ResourceContents, error)
|
||||
|
||||
mimeType := ExtractString(contentMap, "mimeType")
|
||||
|
||||
meta := ExtractMap(contentMap, "_meta")
|
||||
|
||||
if _, present := contentMap["_meta"]; present && meta == nil {
|
||||
return nil, fmt.Errorf("_meta must be an object")
|
||||
}
|
||||
|
||||
if text := ExtractString(contentMap, "text"); text != "" {
|
||||
return TextResourceContents{
|
||||
Meta: meta,
|
||||
URI: uri,
|
||||
MIMEType: mimeType,
|
||||
Text: text,
|
||||
@@ -697,6 +811,7 @@ func ParseResourceContents(contentMap map[string]any) (ResourceContents, error)
|
||||
|
||||
if blob := ExtractString(contentMap, "blob"); blob != "" {
|
||||
return BlobResourceContents{
|
||||
Meta: meta,
|
||||
URI: uri,
|
||||
MIMEType: mimeType,
|
||||
Blob: blob,
|
||||
@@ -861,3 +976,224 @@ func ParseStringMap(request CallToolRequest, key string, defaultValue map[string
|
||||
func ToBoolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
// ToInt64Ptr returns a pointer to the given int64 value
|
||||
func ToInt64Ptr(i int64) *int64 {
|
||||
return &i
|
||||
}
|
||||
|
||||
// GetTextFromContent extracts text from a Content interface that might be a TextContent struct
|
||||
// or a map[string]any that was unmarshaled from JSON. This is useful when dealing with content
|
||||
// that comes from different transport layers that may handle JSON differently.
|
||||
//
|
||||
// This function uses fallback behavior for non-text content - it returns a string representation
|
||||
// via fmt.Sprintf for any content that cannot be extracted as text. This is a lossy operation
|
||||
// intended for convenience in logging and display scenarios.
|
||||
//
|
||||
// For strict type validation, use ParseContent() instead, which returns an error for invalid content.
|
||||
func GetTextFromContent(content any) string {
|
||||
switch c := content.(type) {
|
||||
case TextContent:
|
||||
return c.Text
|
||||
case map[string]any:
|
||||
// Handle JSON unmarshaled content
|
||||
if contentType, exists := c["type"]; exists && contentType == "text" {
|
||||
if text, exists := c["text"].(string); exists {
|
||||
return text
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%v", content)
|
||||
case string:
|
||||
return c
|
||||
default:
|
||||
return fmt.Sprintf("%v", content)
|
||||
}
|
||||
}
|
||||
|
||||
// jsonToTask convert json content to GetTaskResult structure
|
||||
func jsonToTask(jsonContent map[string]any, result *GetTaskResult) {
|
||||
taskId, ok := jsonContent["taskId"]
|
||||
if ok {
|
||||
if taskIdStr, ok := taskId.(string); ok {
|
||||
result.TaskId = taskIdStr
|
||||
}
|
||||
}
|
||||
|
||||
taskStatus, ok := jsonContent["status"]
|
||||
if ok {
|
||||
if taskStatusStr, ok := taskStatus.(string); ok {
|
||||
result.Status = TaskStatus(taskStatusStr)
|
||||
}
|
||||
}
|
||||
|
||||
taskStatusMessage, ok := jsonContent["statusMessage"]
|
||||
if ok {
|
||||
if taskStatusMessageStr, ok := taskStatusMessage.(string); ok {
|
||||
result.StatusMessage = taskStatusMessageStr
|
||||
}
|
||||
}
|
||||
|
||||
createdAt, ok := jsonContent["createdAt"]
|
||||
if ok {
|
||||
if createdAtStr, ok := createdAt.(string); ok {
|
||||
result.CreatedAt = createdAtStr
|
||||
}
|
||||
}
|
||||
|
||||
lastUpdatedAt, ok := jsonContent["lastUpdatedAt"]
|
||||
if ok {
|
||||
if lastUpdatedAtStr, ok := lastUpdatedAt.(string); ok {
|
||||
result.LastUpdatedAt = lastUpdatedAtStr
|
||||
}
|
||||
}
|
||||
|
||||
ttl, ok := jsonContent["ttl"]
|
||||
if ok {
|
||||
if ttlFloat, ok := ttl.(float64); ok {
|
||||
ttlInt64 := int64(ttlFloat)
|
||||
result.TTL = &ttlInt64
|
||||
}
|
||||
}
|
||||
|
||||
pollInterval, ok := jsonContent["pollInterval"]
|
||||
if ok {
|
||||
if pollIntervalFloat64, ok := pollInterval.(float64); ok {
|
||||
pollIntervalInt := int64(pollIntervalFloat64)
|
||||
result.PollInterval = &pollIntervalInt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ParseCancelTaskResult parses a JSON message and converts it to a CancelTaskResult.
|
||||
func ParseCancelTaskResult(rawMessage *json.RawMessage) (*CancelTaskResult, error) {
|
||||
if rawMessage == nil {
|
||||
return nil, fmt.Errorf("response is nil")
|
||||
}
|
||||
|
||||
var jsonContent map[string]any
|
||||
if err := json.Unmarshal(*rawMessage, &jsonContent); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
convertResult := GetTaskResult{}
|
||||
jsonToTask(jsonContent, &convertResult)
|
||||
cancelResult := CancelTaskResult(convertResult)
|
||||
|
||||
meta, ok := jsonContent["_meta"]
|
||||
if ok {
|
||||
if metaMap, ok := meta.(map[string]any); ok {
|
||||
cancelResult.Meta = NewMetaFromMap(metaMap)
|
||||
}
|
||||
}
|
||||
|
||||
return &cancelResult, nil
|
||||
}
|
||||
|
||||
// ParseListTasksResult parses a JSON message and converts it to a ListTasksResult.
|
||||
func ParseListTasksResult(rawMessage *json.RawMessage) (*ListTasksResult, error) {
|
||||
if rawMessage == nil {
|
||||
return nil, fmt.Errorf("response is nil")
|
||||
}
|
||||
|
||||
var jsonContent map[string]any
|
||||
if err := json.Unmarshal(*rawMessage, &jsonContent); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
listTasksResult := ListTasksResult{}
|
||||
|
||||
meta, ok := jsonContent["_meta"]
|
||||
if ok {
|
||||
if metaMap, ok := meta.(map[string]any); ok {
|
||||
listTasksResult.Meta = NewMetaFromMap(metaMap)
|
||||
}
|
||||
}
|
||||
|
||||
tasks, ok := jsonContent["tasks"]
|
||||
if ok {
|
||||
if taskArr, ok := tasks.([]any); ok {
|
||||
for _, task := range taskArr {
|
||||
if taskJsonContent, ok := task.(map[string]any); ok {
|
||||
getTaskResult := GetTaskResult{}
|
||||
jsonToTask(taskJsonContent, &getTaskResult)
|
||||
listTasksResult.Tasks = append(listTasksResult.Tasks, getTaskResult.Task)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextCursor, ok := jsonContent["nextCursor"]
|
||||
if ok {
|
||||
if cursorStr, ok := nextCursor.(string); ok {
|
||||
listTasksResult.NextCursor = Cursor(cursorStr)
|
||||
}
|
||||
}
|
||||
|
||||
return &listTasksResult, nil
|
||||
}
|
||||
|
||||
// ParseTaskResultResult parses a JSON message and converts it to a TaskResultResult.
|
||||
func ParseTaskResultResult(rawMessage *json.RawMessage) (*TaskResultResult, error) {
|
||||
if rawMessage == nil {
|
||||
return nil, fmt.Errorf("response is nil")
|
||||
}
|
||||
|
||||
var jsonContent map[string]any
|
||||
if err := json.Unmarshal(*rawMessage, &jsonContent); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
resultResult := TaskResultResult{}
|
||||
meta, ok := jsonContent["_meta"]
|
||||
if ok {
|
||||
if metaMap, ok := meta.(map[string]any); ok {
|
||||
resultResult.Meta = NewMetaFromMap(metaMap)
|
||||
}
|
||||
}
|
||||
|
||||
result, ok := jsonContent["result"]
|
||||
if ok {
|
||||
if resultMap, ok := result.(map[string]any); ok {
|
||||
if isError, ok := resultMap["isError"].(bool); ok {
|
||||
resultResult.IsError = isError
|
||||
}
|
||||
if contents, ok := resultMap["content"].([]any); ok {
|
||||
for _, content := range contents {
|
||||
if contentMap, ok := content.(map[string]any); ok {
|
||||
parsedContent, err := ParseContent(contentMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resultResult.Content = append(resultResult.Content, parsedContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &resultResult, nil
|
||||
}
|
||||
|
||||
// ParseGetTaskResult parses a JSON message and converts it to a GetTaskResult.
|
||||
func ParseGetTaskResult(rawMessage *json.RawMessage) (*GetTaskResult, error) {
|
||||
if rawMessage == nil {
|
||||
return nil, fmt.Errorf("response is nil")
|
||||
}
|
||||
|
||||
var jsonContent map[string]any
|
||||
if err := json.Unmarshal(*rawMessage, &jsonContent); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
result := GetTaskResult{}
|
||||
meta, ok := jsonContent["_meta"]
|
||||
if ok {
|
||||
if metaMap, ok := meta.(map[string]any); ok {
|
||||
result.Meta = NewMetaFromMap(metaMap)
|
||||
}
|
||||
}
|
||||
|
||||
jsonToTask(jsonContent, &result)
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
35
vendor/github.com/mark3labs/mcp-go/server/completion.go
generated
vendored
Normal file
35
vendor/github.com/mark3labs/mcp-go/server/completion.go
generated
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
type PromptCompletionProvider interface {
|
||||
// CompletePromptArgument provides completions for a prompt argument
|
||||
CompletePromptArgument(ctx context.Context, promptName string, argument mcp.CompleteArgument, context mcp.CompleteContext) (*mcp.Completion, error)
|
||||
}
|
||||
|
||||
type ResourceCompletionProvider interface {
|
||||
// CompleteResourceArgument provides completions for a resource template argument
|
||||
CompleteResourceArgument(ctx context.Context, uri string, argument mcp.CompleteArgument, context mcp.CompleteContext) (*mcp.Completion, error)
|
||||
}
|
||||
|
||||
// DefaultCompletionProvider returns no completions (fallback)
|
||||
type DefaultPromptCompletionProvider struct{}
|
||||
|
||||
func (p *DefaultPromptCompletionProvider) CompletePromptArgument(ctx context.Context, promptName string, argument mcp.CompleteArgument, context mcp.CompleteContext) (*mcp.Completion, error) {
|
||||
return &mcp.Completion{
|
||||
Values: []string{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DefaultResourceCompletionProvider returns no completions (fallback)
|
||||
type DefaultResourceCompletionProvider struct{}
|
||||
|
||||
func (p *DefaultResourceCompletionProvider) CompleteResourceArgument(ctx context.Context, uri string, argument mcp.CompleteArgument, context mcp.CompleteContext) (*mcp.Completion, error) {
|
||||
return &mcp.Completion{
|
||||
Values: []string{},
|
||||
}, nil
|
||||
}
|
||||
87
vendor/github.com/mark3labs/mcp-go/server/elicitation.go
generated
vendored
Normal file
87
vendor/github.com/mark3labs/mcp-go/server/elicitation.go
generated
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNoActiveSession is returned when there is no active session in the context
|
||||
ErrNoActiveSession = errors.New("no active session")
|
||||
// ErrElicitationNotSupported is returned when the session does not support elicitation
|
||||
ErrElicitationNotSupported = errors.New("session does not support elicitation")
|
||||
)
|
||||
|
||||
// RequestElicitation sends an elicitation request to the client.
|
||||
// The client must have declared elicitation capability during initialization.
|
||||
// The session must implement SessionWithElicitation to support this operation.
|
||||
func (s *MCPServer) RequestElicitation(ctx context.Context, request mcp.ElicitationRequest) (*mcp.ElicitationResult, error) {
|
||||
session := ClientSessionFromContext(ctx)
|
||||
if session == nil {
|
||||
return nil, ErrNoActiveSession
|
||||
}
|
||||
|
||||
// Check if the session supports elicitation requests
|
||||
if elicitationSession, ok := session.(SessionWithElicitation); ok {
|
||||
if err := request.Params.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return elicitationSession.RequestElicitation(ctx, request)
|
||||
}
|
||||
|
||||
return nil, ErrElicitationNotSupported
|
||||
}
|
||||
|
||||
// RequestURLElicitation sends a URL mode elicitation request to the client.
|
||||
// This is used when the server needs the user to perform an out-of-band interaction.
|
||||
func (s *MCPServer) RequestURLElicitation(
|
||||
ctx context.Context,
|
||||
session ClientSession,
|
||||
elicitationID string,
|
||||
url string,
|
||||
message string,
|
||||
) (*mcp.ElicitationResult, error) {
|
||||
if session == nil {
|
||||
return nil, ErrNoActiveSession
|
||||
}
|
||||
|
||||
params := mcp.ElicitationParams{
|
||||
Mode: mcp.ElicitationModeURL,
|
||||
Message: message,
|
||||
ElicitationID: elicitationID,
|
||||
URL: url,
|
||||
}
|
||||
|
||||
if err := params.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request := mcp.ElicitationRequest{
|
||||
Request: mcp.Request{
|
||||
Method: string(mcp.MethodElicitationCreate),
|
||||
},
|
||||
Params: params,
|
||||
}
|
||||
|
||||
if elicitationSession, ok := session.(SessionWithElicitation); ok {
|
||||
return elicitationSession.RequestElicitation(ctx, request)
|
||||
}
|
||||
return nil, ErrElicitationNotSupported
|
||||
}
|
||||
|
||||
// SendElicitationComplete sends a notification that a URL mode elicitation has completed
|
||||
// SendElicitationComplete sends a notification that a URL mode elicitation has completed
|
||||
func (s *MCPServer) SendElicitationComplete(
|
||||
ctx context.Context,
|
||||
session ClientSession,
|
||||
elicitationID string,
|
||||
) error {
|
||||
if session == nil {
|
||||
return ErrNoActiveSession
|
||||
}
|
||||
|
||||
jsonRPCNotif := mcp.NewElicitationCompleteNotification(elicitationID)
|
||||
return s.sendNotificationCore(ctx, session, jsonRPCNotif)
|
||||
}
|
||||
12
vendor/github.com/mark3labs/mcp-go/server/errors.go
generated
vendored
12
vendor/github.com/mark3labs/mcp-go/server/errors.go
generated
vendored
@@ -13,11 +13,13 @@ var (
|
||||
ErrToolNotFound = errors.New("tool not found")
|
||||
|
||||
// Session-related errors
|
||||
ErrSessionNotFound = errors.New("session not found")
|
||||
ErrSessionExists = errors.New("session already exists")
|
||||
ErrSessionNotInitialized = errors.New("session not properly initialized")
|
||||
ErrSessionDoesNotSupportTools = errors.New("session does not support per-session tools")
|
||||
ErrSessionDoesNotSupportLogging = errors.New("session does not support setting logging level")
|
||||
ErrSessionNotFound = errors.New("session not found")
|
||||
ErrSessionExists = errors.New("session already exists")
|
||||
ErrSessionNotInitialized = errors.New("session not properly initialized")
|
||||
ErrSessionDoesNotSupportTools = errors.New("session does not support per-session tools")
|
||||
ErrSessionDoesNotSupportResources = errors.New("session does not support per-session resources")
|
||||
ErrSessionDoesNotSupportResourceTemplates = errors.New("session does not support resource templates")
|
||||
ErrSessionDoesNotSupportLogging = errors.New("session does not support setting logging level")
|
||||
|
||||
// Notification-related errors
|
||||
ErrNotificationNotInitialized = errors.New("notification channel not initialized")
|
||||
|
||||
164
vendor/github.com/mark3labs/mcp-go/server/hooks.go
generated
vendored
164
vendor/github.com/mark3labs/mcp-go/server/hooks.go
generated
vendored
@@ -89,7 +89,22 @@ type OnBeforeListToolsFunc func(ctx context.Context, id any, message *mcp.ListTo
|
||||
type OnAfterListToolsFunc func(ctx context.Context, id any, message *mcp.ListToolsRequest, result *mcp.ListToolsResult)
|
||||
|
||||
type OnBeforeCallToolFunc func(ctx context.Context, id any, message *mcp.CallToolRequest)
|
||||
type OnAfterCallToolFunc func(ctx context.Context, id any, message *mcp.CallToolRequest, result *mcp.CallToolResult)
|
||||
type OnAfterCallToolFunc func(ctx context.Context, id any, message *mcp.CallToolRequest, result any)
|
||||
|
||||
type OnBeforeGetTaskFunc func(ctx context.Context, id any, message *mcp.GetTaskRequest)
|
||||
type OnAfterGetTaskFunc func(ctx context.Context, id any, message *mcp.GetTaskRequest, result *mcp.GetTaskResult)
|
||||
|
||||
type OnBeforeListTasksFunc func(ctx context.Context, id any, message *mcp.ListTasksRequest)
|
||||
type OnAfterListTasksFunc func(ctx context.Context, id any, message *mcp.ListTasksRequest, result *mcp.ListTasksResult)
|
||||
|
||||
type OnBeforeTaskResultFunc func(ctx context.Context, id any, message *mcp.TaskResultRequest)
|
||||
type OnAfterTaskResultFunc func(ctx context.Context, id any, message *mcp.TaskResultRequest, result *mcp.TaskResultResult)
|
||||
|
||||
type OnBeforeCancelTaskFunc func(ctx context.Context, id any, message *mcp.CancelTaskRequest)
|
||||
type OnAfterCancelTaskFunc func(ctx context.Context, id any, message *mcp.CancelTaskRequest, result *mcp.CancelTaskResult)
|
||||
|
||||
type OnBeforeCompleteFunc func(ctx context.Context, id any, message *mcp.CompleteRequest)
|
||||
type OnAfterCompleteFunc func(ctx context.Context, id any, message *mcp.CompleteRequest, result *mcp.CompleteResult)
|
||||
|
||||
type Hooks struct {
|
||||
OnRegisterSession []OnRegisterSessionHookFunc
|
||||
@@ -118,6 +133,16 @@ type Hooks struct {
|
||||
OnAfterListTools []OnAfterListToolsFunc
|
||||
OnBeforeCallTool []OnBeforeCallToolFunc
|
||||
OnAfterCallTool []OnAfterCallToolFunc
|
||||
OnBeforeGetTask []OnBeforeGetTaskFunc
|
||||
OnAfterGetTask []OnAfterGetTaskFunc
|
||||
OnBeforeListTasks []OnBeforeListTasksFunc
|
||||
OnAfterListTasks []OnAfterListTasksFunc
|
||||
OnBeforeTaskResult []OnBeforeTaskResultFunc
|
||||
OnAfterTaskResult []OnAfterTaskResultFunc
|
||||
OnBeforeCancelTask []OnBeforeCancelTaskFunc
|
||||
OnAfterCancelTask []OnAfterCancelTaskFunc
|
||||
OnBeforeComplete []OnBeforeCompleteFunc
|
||||
OnAfterComplete []OnAfterCompleteFunc
|
||||
}
|
||||
|
||||
func (c *Hooks) AddBeforeAny(hook BeforeAnyHookFunc) {
|
||||
@@ -521,7 +546,7 @@ func (c *Hooks) beforeCallTool(ctx context.Context, id any, message *mcp.CallToo
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Hooks) afterCallTool(ctx context.Context, id any, message *mcp.CallToolRequest, result *mcp.CallToolResult) {
|
||||
func (c *Hooks) afterCallTool(ctx context.Context, id any, message *mcp.CallToolRequest, result any) {
|
||||
c.onSuccess(ctx, id, mcp.MethodToolsCall, message, result)
|
||||
if c == nil {
|
||||
return
|
||||
@@ -530,3 +555,138 @@ func (c *Hooks) afterCallTool(ctx context.Context, id any, message *mcp.CallTool
|
||||
hook(ctx, id, message, result)
|
||||
}
|
||||
}
|
||||
func (c *Hooks) AddBeforeGetTask(hook OnBeforeGetTaskFunc) {
|
||||
c.OnBeforeGetTask = append(c.OnBeforeGetTask, hook)
|
||||
}
|
||||
|
||||
func (c *Hooks) AddAfterGetTask(hook OnAfterGetTaskFunc) {
|
||||
c.OnAfterGetTask = append(c.OnAfterGetTask, hook)
|
||||
}
|
||||
|
||||
func (c *Hooks) beforeGetTask(ctx context.Context, id any, message *mcp.GetTaskRequest) {
|
||||
c.beforeAny(ctx, id, mcp.MethodTasksGet, message)
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
for _, hook := range c.OnBeforeGetTask {
|
||||
hook(ctx, id, message)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Hooks) afterGetTask(ctx context.Context, id any, message *mcp.GetTaskRequest, result *mcp.GetTaskResult) {
|
||||
c.onSuccess(ctx, id, mcp.MethodTasksGet, message, result)
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
for _, hook := range c.OnAfterGetTask {
|
||||
hook(ctx, id, message, result)
|
||||
}
|
||||
}
|
||||
func (c *Hooks) AddBeforeListTasks(hook OnBeforeListTasksFunc) {
|
||||
c.OnBeforeListTasks = append(c.OnBeforeListTasks, hook)
|
||||
}
|
||||
|
||||
func (c *Hooks) AddAfterListTasks(hook OnAfterListTasksFunc) {
|
||||
c.OnAfterListTasks = append(c.OnAfterListTasks, hook)
|
||||
}
|
||||
|
||||
func (c *Hooks) beforeListTasks(ctx context.Context, id any, message *mcp.ListTasksRequest) {
|
||||
c.beforeAny(ctx, id, mcp.MethodTasksList, message)
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
for _, hook := range c.OnBeforeListTasks {
|
||||
hook(ctx, id, message)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Hooks) afterListTasks(ctx context.Context, id any, message *mcp.ListTasksRequest, result *mcp.ListTasksResult) {
|
||||
c.onSuccess(ctx, id, mcp.MethodTasksList, message, result)
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
for _, hook := range c.OnAfterListTasks {
|
||||
hook(ctx, id, message, result)
|
||||
}
|
||||
}
|
||||
func (c *Hooks) AddBeforeTaskResult(hook OnBeforeTaskResultFunc) {
|
||||
c.OnBeforeTaskResult = append(c.OnBeforeTaskResult, hook)
|
||||
}
|
||||
|
||||
func (c *Hooks) AddAfterTaskResult(hook OnAfterTaskResultFunc) {
|
||||
c.OnAfterTaskResult = append(c.OnAfterTaskResult, hook)
|
||||
}
|
||||
|
||||
func (c *Hooks) beforeTaskResult(ctx context.Context, id any, message *mcp.TaskResultRequest) {
|
||||
c.beforeAny(ctx, id, mcp.MethodTasksResult, message)
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
for _, hook := range c.OnBeforeTaskResult {
|
||||
hook(ctx, id, message)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Hooks) afterTaskResult(ctx context.Context, id any, message *mcp.TaskResultRequest, result *mcp.TaskResultResult) {
|
||||
c.onSuccess(ctx, id, mcp.MethodTasksResult, message, result)
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
for _, hook := range c.OnAfterTaskResult {
|
||||
hook(ctx, id, message, result)
|
||||
}
|
||||
}
|
||||
func (c *Hooks) AddBeforeCancelTask(hook OnBeforeCancelTaskFunc) {
|
||||
c.OnBeforeCancelTask = append(c.OnBeforeCancelTask, hook)
|
||||
}
|
||||
|
||||
func (c *Hooks) AddAfterCancelTask(hook OnAfterCancelTaskFunc) {
|
||||
c.OnAfterCancelTask = append(c.OnAfterCancelTask, hook)
|
||||
}
|
||||
|
||||
func (c *Hooks) beforeCancelTask(ctx context.Context, id any, message *mcp.CancelTaskRequest) {
|
||||
c.beforeAny(ctx, id, mcp.MethodTasksCancel, message)
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
for _, hook := range c.OnBeforeCancelTask {
|
||||
hook(ctx, id, message)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Hooks) afterCancelTask(ctx context.Context, id any, message *mcp.CancelTaskRequest, result *mcp.CancelTaskResult) {
|
||||
c.onSuccess(ctx, id, mcp.MethodTasksCancel, message, result)
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
for _, hook := range c.OnAfterCancelTask {
|
||||
hook(ctx, id, message, result)
|
||||
}
|
||||
}
|
||||
func (c *Hooks) AddBeforeComplete(hook OnBeforeCompleteFunc) {
|
||||
c.OnBeforeComplete = append(c.OnBeforeComplete, hook)
|
||||
}
|
||||
|
||||
func (c *Hooks) AddAfterComplete(hook OnAfterCompleteFunc) {
|
||||
c.OnAfterComplete = append(c.OnAfterComplete, hook)
|
||||
}
|
||||
|
||||
func (c *Hooks) beforeComplete(ctx context.Context, id any, message *mcp.CompleteRequest) {
|
||||
c.beforeAny(ctx, id, mcp.MethodCompletionComplete, message)
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
for _, hook := range c.OnBeforeComplete {
|
||||
hook(ctx, id, message)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Hooks) afterComplete(ctx context.Context, id any, message *mcp.CompleteRequest, result *mcp.CompleteResult) {
|
||||
c.onSuccess(ctx, id, mcp.MethodCompletionComplete, message, result)
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
for _, hook := range c.OnAfterComplete {
|
||||
hook(ctx, id, message, result)
|
||||
}
|
||||
}
|
||||
|
||||
58
vendor/github.com/mark3labs/mcp-go/server/inprocess_session.go
generated
vendored
58
vendor/github.com/mark3labs/mcp-go/server/inprocess_session.go
generated
vendored
@@ -15,6 +15,16 @@ type SamplingHandler interface {
|
||||
CreateMessage(ctx context.Context, request mcp.CreateMessageRequest) (*mcp.CreateMessageResult, error)
|
||||
}
|
||||
|
||||
// ElicitationHandler defines the interface for handling elicitation requests from servers.
|
||||
type ElicitationHandler interface {
|
||||
Elicit(ctx context.Context, request mcp.ElicitationRequest) (*mcp.ElicitationResult, error)
|
||||
}
|
||||
|
||||
// RootsHandler defines the interface for handling roots list requests from servers.
|
||||
type RootsHandler interface {
|
||||
ListRoots(ctx context.Context, request mcp.ListRootsRequest) (*mcp.ListRootsResult, error)
|
||||
}
|
||||
|
||||
type InProcessSession struct {
|
||||
sessionID string
|
||||
notifications chan mcp.JSONRPCNotification
|
||||
@@ -23,6 +33,8 @@ type InProcessSession struct {
|
||||
clientInfo atomic.Value
|
||||
clientCapabilities atomic.Value
|
||||
samplingHandler SamplingHandler
|
||||
elicitationHandler ElicitationHandler
|
||||
rootsHandler RootsHandler
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
@@ -34,6 +46,16 @@ func NewInProcessSession(sessionID string, samplingHandler SamplingHandler) *InP
|
||||
}
|
||||
}
|
||||
|
||||
func NewInProcessSessionWithHandlers(sessionID string, samplingHandler SamplingHandler, elicitationHandler ElicitationHandler, rootsHandler RootsHandler) *InProcessSession {
|
||||
return &InProcessSession{
|
||||
sessionID: sessionID,
|
||||
notifications: make(chan mcp.JSONRPCNotification, 100),
|
||||
samplingHandler: samplingHandler,
|
||||
elicitationHandler: elicitationHandler,
|
||||
rootsHandler: rootsHandler,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *InProcessSession) SessionID() string {
|
||||
return s.sessionID
|
||||
}
|
||||
@@ -101,6 +123,32 @@ func (s *InProcessSession) RequestSampling(ctx context.Context, request mcp.Crea
|
||||
return handler.CreateMessage(ctx, request)
|
||||
}
|
||||
|
||||
func (s *InProcessSession) RequestElicitation(ctx context.Context, request mcp.ElicitationRequest) (*mcp.ElicitationResult, error) {
|
||||
s.mu.RLock()
|
||||
handler := s.elicitationHandler
|
||||
s.mu.RUnlock()
|
||||
|
||||
if handler == nil {
|
||||
return nil, fmt.Errorf("no elicitation handler available")
|
||||
}
|
||||
|
||||
return handler.Elicit(ctx, request)
|
||||
}
|
||||
|
||||
// ListRoots sends a list roots request to the client and waits for the response.
|
||||
// Returns an error if no roots handler is available.
|
||||
func (s *InProcessSession) ListRoots(ctx context.Context, request mcp.ListRootsRequest) (*mcp.ListRootsResult, error) {
|
||||
s.mu.RLock()
|
||||
handler := s.rootsHandler
|
||||
s.mu.RUnlock()
|
||||
|
||||
if handler == nil {
|
||||
return nil, fmt.Errorf("no roots handler available")
|
||||
}
|
||||
|
||||
return handler.ListRoots(ctx, request)
|
||||
}
|
||||
|
||||
// GenerateInProcessSessionID generates a unique session ID for inprocess clients
|
||||
func GenerateInProcessSessionID() string {
|
||||
return fmt.Sprintf("inprocess-%d", time.Now().UnixNano())
|
||||
@@ -108,8 +156,10 @@ func GenerateInProcessSessionID() string {
|
||||
|
||||
// Ensure interface compliance
|
||||
var (
|
||||
_ ClientSession = (*InProcessSession)(nil)
|
||||
_ SessionWithLogging = (*InProcessSession)(nil)
|
||||
_ SessionWithClientInfo = (*InProcessSession)(nil)
|
||||
_ SessionWithSampling = (*InProcessSession)(nil)
|
||||
_ ClientSession = (*InProcessSession)(nil)
|
||||
_ SessionWithLogging = (*InProcessSession)(nil)
|
||||
_ SessionWithClientInfo = (*InProcessSession)(nil)
|
||||
_ SessionWithSampling = (*InProcessSession)(nil)
|
||||
_ SessionWithElicitation = (*InProcessSession)(nil)
|
||||
_ SessionWithRoots = (*InProcessSession)(nil)
|
||||
)
|
||||
|
||||
144
vendor/github.com/mark3labs/mcp-go/server/request_handler.go
generated
vendored
144
vendor/github.com/mark3labs/mcp-go/server/request_handler.go
generated
vendored
@@ -80,6 +80,18 @@ func (s *MCPServer) HandleMessage(
|
||||
headers = make(http.Header)
|
||||
}
|
||||
|
||||
// Wrap context with cancel for in-flight request cancellation (MCP spec: notifications/cancelled)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// Store cancel func so notifications/cancelled can cancel this request.
|
||||
// Use session-scoped keys to prevent cross-session request ID collisions.
|
||||
if baseMessage.ID != nil {
|
||||
key := inflightKey(ctx, baseMessage.ID)
|
||||
s.inflightCancels.Store(key, cancel)
|
||||
defer s.inflightCancels.Delete(key)
|
||||
}
|
||||
|
||||
switch baseMessage.Method {
|
||||
case mcp.MethodInitialize:
|
||||
var request mcp.InitializeRequest
|
||||
@@ -305,7 +317,7 @@ func (s *MCPServer) HandleMessage(
|
||||
return createResponse(baseMessage.ID, *result)
|
||||
case mcp.MethodToolsCall:
|
||||
var request mcp.CallToolRequest
|
||||
var result *mcp.CallToolResult
|
||||
var result any
|
||||
if s.capabilities.tools == nil {
|
||||
err = &requestError{
|
||||
id: baseMessage.ID,
|
||||
@@ -328,6 +340,136 @@ func (s *MCPServer) HandleMessage(
|
||||
return err.ToJSONRPCError()
|
||||
}
|
||||
s.hooks.afterCallTool(ctx, baseMessage.ID, &request, result)
|
||||
return createResponse(baseMessage.ID, result)
|
||||
case mcp.MethodTasksGet:
|
||||
var request mcp.GetTaskRequest
|
||||
var result *mcp.GetTaskResult
|
||||
if s.capabilities.tasks == nil {
|
||||
err = &requestError{
|
||||
id: baseMessage.ID,
|
||||
code: mcp.METHOD_NOT_FOUND,
|
||||
err: fmt.Errorf("tasks %w", ErrUnsupported),
|
||||
}
|
||||
} else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil {
|
||||
err = &requestError{
|
||||
id: baseMessage.ID,
|
||||
code: mcp.INVALID_REQUEST,
|
||||
err: &UnparsableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method},
|
||||
}
|
||||
} else {
|
||||
request.Header = headers
|
||||
s.hooks.beforeGetTask(ctx, baseMessage.ID, &request)
|
||||
result, err = s.handleGetTask(ctx, baseMessage.ID, request)
|
||||
}
|
||||
if err != nil {
|
||||
s.hooks.onError(ctx, baseMessage.ID, baseMessage.Method, &request, err)
|
||||
return err.ToJSONRPCError()
|
||||
}
|
||||
s.hooks.afterGetTask(ctx, baseMessage.ID, &request, result)
|
||||
return createResponse(baseMessage.ID, *result)
|
||||
case mcp.MethodTasksList:
|
||||
var request mcp.ListTasksRequest
|
||||
var result *mcp.ListTasksResult
|
||||
if s.capabilities.tasks == nil {
|
||||
err = &requestError{
|
||||
id: baseMessage.ID,
|
||||
code: mcp.METHOD_NOT_FOUND,
|
||||
err: fmt.Errorf("tasks %w", ErrUnsupported),
|
||||
}
|
||||
} else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil {
|
||||
err = &requestError{
|
||||
id: baseMessage.ID,
|
||||
code: mcp.INVALID_REQUEST,
|
||||
err: &UnparsableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method},
|
||||
}
|
||||
} else {
|
||||
request.Header = headers
|
||||
s.hooks.beforeListTasks(ctx, baseMessage.ID, &request)
|
||||
result, err = s.handleListTasks(ctx, baseMessage.ID, request)
|
||||
}
|
||||
if err != nil {
|
||||
s.hooks.onError(ctx, baseMessage.ID, baseMessage.Method, &request, err)
|
||||
return err.ToJSONRPCError()
|
||||
}
|
||||
s.hooks.afterListTasks(ctx, baseMessage.ID, &request, result)
|
||||
return createResponse(baseMessage.ID, *result)
|
||||
case mcp.MethodTasksResult:
|
||||
var request mcp.TaskResultRequest
|
||||
var result *mcp.TaskResultResult
|
||||
if s.capabilities.tasks == nil {
|
||||
err = &requestError{
|
||||
id: baseMessage.ID,
|
||||
code: mcp.METHOD_NOT_FOUND,
|
||||
err: fmt.Errorf("tasks %w", ErrUnsupported),
|
||||
}
|
||||
} else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil {
|
||||
err = &requestError{
|
||||
id: baseMessage.ID,
|
||||
code: mcp.INVALID_REQUEST,
|
||||
err: &UnparsableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method},
|
||||
}
|
||||
} else {
|
||||
request.Header = headers
|
||||
s.hooks.beforeTaskResult(ctx, baseMessage.ID, &request)
|
||||
result, err = s.handleTaskResult(ctx, baseMessage.ID, request)
|
||||
}
|
||||
if err != nil {
|
||||
s.hooks.onError(ctx, baseMessage.ID, baseMessage.Method, &request, err)
|
||||
return err.ToJSONRPCError()
|
||||
}
|
||||
s.hooks.afterTaskResult(ctx, baseMessage.ID, &request, result)
|
||||
return createResponse(baseMessage.ID, *result)
|
||||
case mcp.MethodTasksCancel:
|
||||
var request mcp.CancelTaskRequest
|
||||
var result *mcp.CancelTaskResult
|
||||
if s.capabilities.tasks == nil {
|
||||
err = &requestError{
|
||||
id: baseMessage.ID,
|
||||
code: mcp.METHOD_NOT_FOUND,
|
||||
err: fmt.Errorf("tasks %w", ErrUnsupported),
|
||||
}
|
||||
} else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil {
|
||||
err = &requestError{
|
||||
id: baseMessage.ID,
|
||||
code: mcp.INVALID_REQUEST,
|
||||
err: &UnparsableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method},
|
||||
}
|
||||
} else {
|
||||
request.Header = headers
|
||||
s.hooks.beforeCancelTask(ctx, baseMessage.ID, &request)
|
||||
result, err = s.handleCancelTask(ctx, baseMessage.ID, request)
|
||||
}
|
||||
if err != nil {
|
||||
s.hooks.onError(ctx, baseMessage.ID, baseMessage.Method, &request, err)
|
||||
return err.ToJSONRPCError()
|
||||
}
|
||||
s.hooks.afterCancelTask(ctx, baseMessage.ID, &request, result)
|
||||
return createResponse(baseMessage.ID, *result)
|
||||
case mcp.MethodCompletionComplete:
|
||||
var request mcp.CompleteRequest
|
||||
var result *mcp.CompleteResult
|
||||
if s.capabilities.completions == nil {
|
||||
err = &requestError{
|
||||
id: baseMessage.ID,
|
||||
code: mcp.METHOD_NOT_FOUND,
|
||||
err: fmt.Errorf("completions %w", ErrUnsupported),
|
||||
}
|
||||
} else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil {
|
||||
err = &requestError{
|
||||
id: baseMessage.ID,
|
||||
code: mcp.INVALID_REQUEST,
|
||||
err: &UnparsableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method},
|
||||
}
|
||||
} else {
|
||||
request.Header = headers
|
||||
s.hooks.beforeComplete(ctx, baseMessage.ID, &request)
|
||||
result, err = s.handleComplete(ctx, baseMessage.ID, request)
|
||||
}
|
||||
if err != nil {
|
||||
s.hooks.onError(ctx, baseMessage.ID, baseMessage.Method, &request, err)
|
||||
return err.ToJSONRPCError()
|
||||
}
|
||||
s.hooks.afterComplete(ctx, baseMessage.ID, &request, result)
|
||||
return createResponse(baseMessage.ID, *result)
|
||||
default:
|
||||
return createErrorResponse(
|
||||
|
||||
32
vendor/github.com/mark3labs/mcp-go/server/roots.go
generated
vendored
Normal file
32
vendor/github.com/mark3labs/mcp-go/server/roots.go
generated
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNoClientSession is returned when there is no active client session in the context
|
||||
ErrNoClientSession = errors.New("no active client session")
|
||||
// ErrRootsNotSupported is returned when the session does not support roots
|
||||
ErrRootsNotSupported = errors.New("session does not support roots")
|
||||
)
|
||||
|
||||
// RequestRoots sends an list roots request to the client.
|
||||
// The client must have declared roots capability during initialization.
|
||||
// The session must implement SessionWithRoots to support this operation.
|
||||
func (s *MCPServer) RequestRoots(ctx context.Context, request mcp.ListRootsRequest) (*mcp.ListRootsResult, error) {
|
||||
session := ClientSessionFromContext(ctx)
|
||||
if session == nil {
|
||||
return nil, ErrNoClientSession
|
||||
}
|
||||
|
||||
// Check if the session supports roots requests
|
||||
if rootsSession, ok := session.(SessionWithRoots); ok {
|
||||
return rootsSession.ListRoots(ctx, request)
|
||||
}
|
||||
|
||||
return nil, ErrRootsNotSupported
|
||||
}
|
||||
1432
vendor/github.com/mark3labs/mcp-go/server/server.go
generated
vendored
1432
vendor/github.com/mark3labs/mcp-go/server/server.go
generated
vendored
File diff suppressed because it is too large
Load Diff
327
vendor/github.com/mark3labs/mcp-go/server/session.go
generated
vendored
327
vendor/github.com/mark3labs/mcp-go/server/session.go
generated
vendored
@@ -3,6 +3,8 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/url"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
@@ -39,6 +41,28 @@ type SessionWithTools interface {
|
||||
SetSessionTools(tools map[string]ServerTool)
|
||||
}
|
||||
|
||||
// SessionWithResources is an extension of ClientSession that can store session-specific resource data
|
||||
type SessionWithResources interface {
|
||||
ClientSession
|
||||
// GetSessionResources returns the resources specific to this session, if any
|
||||
// This method must be thread-safe for concurrent access
|
||||
GetSessionResources() map[string]ServerResource
|
||||
// SetSessionResources sets resources specific to this session
|
||||
// This method must be thread-safe for concurrent access
|
||||
SetSessionResources(resources map[string]ServerResource)
|
||||
}
|
||||
|
||||
// SessionWithResourceTemplates is an extension of ClientSession that can store session-specific resource template data
|
||||
type SessionWithResourceTemplates interface {
|
||||
ClientSession
|
||||
// GetSessionResourceTemplates returns the resource templates specific to this session, if any
|
||||
// This method must be thread-safe for concurrent access
|
||||
GetSessionResourceTemplates() map[string]ServerResourceTemplate
|
||||
// SetSessionResourceTemplates sets resource templates specific to this session
|
||||
// This method must be thread-safe for concurrent access
|
||||
SetSessionResourceTemplates(templates map[string]ServerResourceTemplate)
|
||||
}
|
||||
|
||||
// SessionWithClientInfo is an extension of ClientSession that can store client info
|
||||
type SessionWithClientInfo interface {
|
||||
ClientSession
|
||||
@@ -52,6 +76,20 @@ type SessionWithClientInfo interface {
|
||||
SetClientCapabilities(clientCapabilities mcp.ClientCapabilities)
|
||||
}
|
||||
|
||||
// SessionWithElicitation is an extension of ClientSession that can send elicitation requests
|
||||
type SessionWithElicitation interface {
|
||||
ClientSession
|
||||
// RequestElicitation sends an elicitation request to the client and waits for response
|
||||
RequestElicitation(ctx context.Context, request mcp.ElicitationRequest) (*mcp.ElicitationResult, error)
|
||||
}
|
||||
|
||||
// SessionWithRoots is an extension of ClientSession that can send list roots requests
|
||||
type SessionWithRoots interface {
|
||||
ClientSession
|
||||
// ListRoots sends an list roots request to the client and waits for response
|
||||
ListRoots(ctx context.Context, request mcp.ListRootsRequest) (*mcp.ListRootsResult, error)
|
||||
}
|
||||
|
||||
// SessionWithStreamableHTTPConfig extends ClientSession to support streamable HTTP transport configurations
|
||||
type SessionWithStreamableHTTPConfig interface {
|
||||
ClientSession
|
||||
@@ -134,6 +172,9 @@ func (s *MCPServer) SendLogMessageToClient(ctx context.Context, notification mcp
|
||||
func (s *MCPServer) sendNotificationToAllClients(notification mcp.JSONRPCNotification) {
|
||||
s.sessions.Range(func(k, v any) bool {
|
||||
if session, ok := v.(ClientSession); ok && session.Initialized() {
|
||||
if sessionWithStreamableHTTPConfig, ok := session.(SessionWithStreamableHTTPConfig); ok {
|
||||
sessionWithStreamableHTTPConfig.UpgradeToSSEWhenReceiveNotification()
|
||||
}
|
||||
select {
|
||||
case session.NotificationChannel() <- notification:
|
||||
// Successfully sent notification
|
||||
@@ -341,9 +382,7 @@ func (s *MCPServer) AddSessionTools(sessionID string, tools ...ServerTool) error
|
||||
newSessionTools := make(map[string]ServerTool, len(sessionTools)+len(tools))
|
||||
|
||||
// Copy existing tools
|
||||
for k, v := range sessionTools {
|
||||
newSessionTools[k] = v
|
||||
}
|
||||
maps.Copy(newSessionTools, sessionTools)
|
||||
|
||||
// Add new tools
|
||||
for _, tool := range tools {
|
||||
@@ -403,9 +442,7 @@ func (s *MCPServer) DeleteSessionTools(sessionID string, names ...string) error
|
||||
newSessionTools := make(map[string]ServerTool, len(sessionTools))
|
||||
|
||||
// Copy existing tools except those being deleted
|
||||
for k, v := range sessionTools {
|
||||
newSessionTools[k] = v
|
||||
}
|
||||
maps.Copy(newSessionTools, sessionTools)
|
||||
|
||||
// Remove specified tools
|
||||
for _, name := range names {
|
||||
@@ -442,3 +479,281 @@ func (s *MCPServer) DeleteSessionTools(sessionID string, names ...string) error
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddSessionResource adds a resource for a specific session
|
||||
func (s *MCPServer) AddSessionResource(sessionID string, resource mcp.Resource, handler ResourceHandlerFunc) error {
|
||||
return s.AddSessionResources(sessionID, ServerResource{Resource: resource, Handler: handler})
|
||||
}
|
||||
|
||||
// AddSessionResources adds resources for a specific session
|
||||
func (s *MCPServer) AddSessionResources(sessionID string, resources ...ServerResource) error {
|
||||
sessionValue, ok := s.sessions.Load(sessionID)
|
||||
if !ok {
|
||||
return ErrSessionNotFound
|
||||
}
|
||||
|
||||
session, ok := sessionValue.(SessionWithResources)
|
||||
if !ok {
|
||||
return ErrSessionDoesNotSupportResources
|
||||
}
|
||||
|
||||
// For session resources, we want listChanged enabled by default
|
||||
s.implicitlyRegisterCapabilities(
|
||||
func() bool { return s.capabilities.resources != nil },
|
||||
func() { s.capabilities.resources = &resourceCapabilities{listChanged: true} },
|
||||
)
|
||||
|
||||
// Get existing resources (this should return a thread-safe copy)
|
||||
sessionResources := session.GetSessionResources()
|
||||
|
||||
// Create a new map to avoid concurrent modification issues
|
||||
newSessionResources := make(map[string]ServerResource, len(sessionResources)+len(resources))
|
||||
|
||||
// Copy existing resources
|
||||
maps.Copy(newSessionResources, sessionResources)
|
||||
|
||||
// Add new resources with validation
|
||||
for _, resource := range resources {
|
||||
// Validate that URI is non-empty
|
||||
if resource.Resource.URI == "" {
|
||||
return fmt.Errorf("resource URI cannot be empty")
|
||||
}
|
||||
|
||||
// Validate that URI conforms to RFC 3986
|
||||
if _, err := url.ParseRequestURI(resource.Resource.URI); err != nil {
|
||||
return fmt.Errorf("invalid resource URI: %w", err)
|
||||
}
|
||||
|
||||
newSessionResources[resource.Resource.URI] = resource
|
||||
}
|
||||
|
||||
// Set the resources (this should be thread-safe)
|
||||
session.SetSessionResources(newSessionResources)
|
||||
|
||||
// It only makes sense to send resource notifications to initialized sessions --
|
||||
// if we're not initialized yet the client can't possibly have sent their
|
||||
// initial resources/list message.
|
||||
//
|
||||
// For initialized sessions, honor resources.listChanged, which is specifically
|
||||
// about whether notifications will be sent or not.
|
||||
// see <https://modelcontextprotocol.io/specification/2025-03-26/server/resources#capabilities>
|
||||
if session.Initialized() && s.capabilities.resources != nil && s.capabilities.resources.listChanged {
|
||||
// Send notification only to this session
|
||||
if err := s.SendNotificationToSpecificClient(sessionID, "notifications/resources/list_changed", nil); err != nil {
|
||||
// Log the error but don't fail the operation
|
||||
// The resources were successfully added, but notification failed
|
||||
if s.hooks != nil && len(s.hooks.OnError) > 0 {
|
||||
hooks := s.hooks
|
||||
go func(sID string, hooks *Hooks) {
|
||||
ctx := context.Background()
|
||||
hooks.onError(ctx, nil, "notification", map[string]any{
|
||||
"method": "notifications/resources/list_changed",
|
||||
"sessionID": sID,
|
||||
}, fmt.Errorf("failed to send notification after adding resources: %w", err))
|
||||
}(sessionID, hooks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSessionResources removes resources from a specific session
|
||||
func (s *MCPServer) DeleteSessionResources(sessionID string, uris ...string) error {
|
||||
sessionValue, ok := s.sessions.Load(sessionID)
|
||||
if !ok {
|
||||
return ErrSessionNotFound
|
||||
}
|
||||
|
||||
session, ok := sessionValue.(SessionWithResources)
|
||||
if !ok {
|
||||
return ErrSessionDoesNotSupportResources
|
||||
}
|
||||
|
||||
// Get existing resources (this should return a thread-safe copy)
|
||||
sessionResources := session.GetSessionResources()
|
||||
if sessionResources == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new map to avoid concurrent modification issues
|
||||
newSessionResources := make(map[string]ServerResource, len(sessionResources))
|
||||
|
||||
// Copy existing resources except those being deleted
|
||||
maps.Copy(newSessionResources, sessionResources)
|
||||
|
||||
// Remove specified resources and track if anything was actually deleted
|
||||
actuallyDeleted := false
|
||||
for _, uri := range uris {
|
||||
if _, exists := newSessionResources[uri]; exists {
|
||||
delete(newSessionResources, uri)
|
||||
actuallyDeleted = true
|
||||
}
|
||||
}
|
||||
|
||||
// Skip no-op write if nothing was actually deleted
|
||||
if !actuallyDeleted {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set the resources (this should be thread-safe)
|
||||
session.SetSessionResources(newSessionResources)
|
||||
|
||||
// It only makes sense to send resource notifications to initialized sessions --
|
||||
// if we're not initialized yet the client can't possibly have sent their
|
||||
// initial resources/list message.
|
||||
//
|
||||
// For initialized sessions, honor resources.listChanged, which is specifically
|
||||
// about whether notifications will be sent or not.
|
||||
// see <https://modelcontextprotocol.io/specification/2025-03-26/server/resources#capabilities>
|
||||
// Only send notification if something was actually deleted
|
||||
if actuallyDeleted && session.Initialized() && s.capabilities.resources != nil && s.capabilities.resources.listChanged {
|
||||
// Send notification only to this session
|
||||
if err := s.SendNotificationToSpecificClient(sessionID, "notifications/resources/list_changed", nil); err != nil {
|
||||
// Log the error but don't fail the operation
|
||||
// The resources were successfully deleted, but notification failed
|
||||
if s.hooks != nil && len(s.hooks.OnError) > 0 {
|
||||
hooks := s.hooks
|
||||
go func(sID string, hooks *Hooks) {
|
||||
ctx := context.Background()
|
||||
hooks.onError(ctx, nil, "notification", map[string]any{
|
||||
"method": "notifications/resources/list_changed",
|
||||
"sessionID": sID,
|
||||
}, fmt.Errorf("failed to send notification after deleting resources: %w", err))
|
||||
}(sessionID, hooks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddSessionResourceTemplate adds a resource template for a specific session
|
||||
func (s *MCPServer) AddSessionResourceTemplate(sessionID string, template mcp.ResourceTemplate, handler ResourceTemplateHandlerFunc) error {
|
||||
return s.AddSessionResourceTemplates(sessionID, ServerResourceTemplate{
|
||||
Template: template,
|
||||
Handler: handler,
|
||||
})
|
||||
}
|
||||
|
||||
// AddSessionResourceTemplates adds resource templates for a specific session
|
||||
func (s *MCPServer) AddSessionResourceTemplates(sessionID string, templates ...ServerResourceTemplate) error {
|
||||
sessionValue, ok := s.sessions.Load(sessionID)
|
||||
if !ok {
|
||||
return ErrSessionNotFound
|
||||
}
|
||||
|
||||
session, ok := sessionValue.(SessionWithResourceTemplates)
|
||||
if !ok {
|
||||
return ErrSessionDoesNotSupportResourceTemplates
|
||||
}
|
||||
|
||||
// For session resource templates, enable listChanged by default
|
||||
// This is the same behavior as session resources
|
||||
s.implicitlyRegisterCapabilities(
|
||||
func() bool { return s.capabilities.resources != nil },
|
||||
func() { s.capabilities.resources = &resourceCapabilities{listChanged: true} },
|
||||
)
|
||||
|
||||
// Get existing templates (this returns a thread-safe copy)
|
||||
sessionTemplates := session.GetSessionResourceTemplates()
|
||||
|
||||
// Create a new map to avoid modifying the returned copy
|
||||
newTemplates := make(map[string]ServerResourceTemplate, len(sessionTemplates)+len(templates))
|
||||
|
||||
// Copy existing templates
|
||||
maps.Copy(newTemplates, sessionTemplates)
|
||||
|
||||
// Validate and add new templates
|
||||
for _, t := range templates {
|
||||
if t.Template.URITemplate == nil {
|
||||
return fmt.Errorf("resource template URITemplate cannot be nil")
|
||||
}
|
||||
raw := t.Template.URITemplate.Raw()
|
||||
if raw == "" {
|
||||
return fmt.Errorf("resource template URITemplate cannot be empty")
|
||||
}
|
||||
if t.Template.Name == "" {
|
||||
return fmt.Errorf("resource template name cannot be empty")
|
||||
}
|
||||
newTemplates[raw] = t
|
||||
}
|
||||
|
||||
// Set the new templates (this method must handle thread-safety)
|
||||
session.SetSessionResourceTemplates(newTemplates)
|
||||
|
||||
// Send notification if the session is initialized and listChanged is enabled
|
||||
if session.Initialized() && s.capabilities.resources != nil && s.capabilities.resources.listChanged {
|
||||
if err := s.SendNotificationToSpecificClient(sessionID, "notifications/resources/list_changed", nil); err != nil {
|
||||
// Log the error but don't fail the operation
|
||||
if s.hooks != nil && len(s.hooks.OnError) > 0 {
|
||||
hooks := s.hooks
|
||||
go func(sID string, hooks *Hooks) {
|
||||
ctx := context.Background()
|
||||
hooks.onError(ctx, nil, "notification", map[string]any{
|
||||
"method": "notifications/resources/list_changed",
|
||||
"sessionID": sID,
|
||||
}, fmt.Errorf("failed to send notification after adding resource templates: %w", err))
|
||||
}(sessionID, hooks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSessionResourceTemplates removes resource templates from a specific session
|
||||
func (s *MCPServer) DeleteSessionResourceTemplates(sessionID string, uriTemplates ...string) error {
|
||||
sessionValue, ok := s.sessions.Load(sessionID)
|
||||
if !ok {
|
||||
return ErrSessionNotFound
|
||||
}
|
||||
|
||||
session, ok := sessionValue.(SessionWithResourceTemplates)
|
||||
if !ok {
|
||||
return ErrSessionDoesNotSupportResourceTemplates
|
||||
}
|
||||
|
||||
// Get existing templates (this returns a thread-safe copy)
|
||||
sessionTemplates := session.GetSessionResourceTemplates()
|
||||
|
||||
// Track if any were actually deleted
|
||||
deletedAny := false
|
||||
|
||||
// Create a new map without the deleted templates
|
||||
newTemplates := make(map[string]ServerResourceTemplate, len(sessionTemplates))
|
||||
maps.Copy(newTemplates, sessionTemplates)
|
||||
|
||||
// Delete specified templates
|
||||
for _, uriTemplate := range uriTemplates {
|
||||
if _, exists := newTemplates[uriTemplate]; exists {
|
||||
delete(newTemplates, uriTemplate)
|
||||
deletedAny = true
|
||||
}
|
||||
}
|
||||
|
||||
// Only update if something was actually deleted
|
||||
if deletedAny {
|
||||
// Set the new templates (this method must handle thread-safety)
|
||||
session.SetSessionResourceTemplates(newTemplates)
|
||||
|
||||
// Send notification if the session is initialized and listChanged is enabled
|
||||
if session.Initialized() && s.capabilities.resources != nil && s.capabilities.resources.listChanged {
|
||||
if err := s.SendNotificationToSpecificClient(sessionID, "notifications/resources/list_changed", nil); err != nil {
|
||||
// Log the error but don't fail the operation
|
||||
if s.hooks != nil && len(s.hooks.OnError) > 0 {
|
||||
hooks := s.hooks
|
||||
go func(sID string, hooks *Hooks) {
|
||||
ctx := context.Background()
|
||||
hooks.onError(ctx, nil, "notification", map[string]any{
|
||||
"method": "notifications/resources/list_changed",
|
||||
"sessionID": sID,
|
||||
}, fmt.Errorf("failed to send notification after deleting resource templates: %w", err))
|
||||
}(sessionID, hooks)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
83
vendor/github.com/mark3labs/mcp-go/server/sse.go
generated
vendored
83
vendor/github.com/mark3labs/mcp-go/server/sse.go
generated
vendored
@@ -29,6 +29,8 @@ type sseSession struct {
|
||||
initialized atomic.Bool
|
||||
loggingLevel atomic.Value
|
||||
tools sync.Map // stores session-specific tools
|
||||
resources sync.Map // stores session-specific resources
|
||||
resourceTemplates sync.Map // stores session-specific resource templates
|
||||
clientInfo atomic.Value // stores session-specific client info
|
||||
clientCapabilities atomic.Value // stores session-specific client capabilities
|
||||
}
|
||||
@@ -45,6 +47,11 @@ type SSEContextFunc func(ctx context.Context, r *http.Request) context.Context
|
||||
// function should return the base path (e.g., "/mcp/tenant123").
|
||||
type DynamicBasePathFunc func(r *http.Request, sessionID string) string
|
||||
|
||||
// SessionIDGenFunc is a function that produces a session ID for a new SSE connection.
|
||||
// It receives the request context and the HTTP request, and should return a session
|
||||
// identifier (string) or an error.
|
||||
type SessionIDGenFunc func(ctx context.Context, r *http.Request) (string, error)
|
||||
|
||||
func (s *sseSession) SessionID() string {
|
||||
return s.sessionID
|
||||
}
|
||||
@@ -75,6 +82,48 @@ func (s *sseSession) GetLogLevel() mcp.LoggingLevel {
|
||||
return level.(mcp.LoggingLevel)
|
||||
}
|
||||
|
||||
func (s *sseSession) GetSessionResources() map[string]ServerResource {
|
||||
resources := make(map[string]ServerResource)
|
||||
s.resources.Range(func(key, value any) bool {
|
||||
if resource, ok := value.(ServerResource); ok {
|
||||
resources[key.(string)] = resource
|
||||
}
|
||||
return true
|
||||
})
|
||||
return resources
|
||||
}
|
||||
|
||||
func (s *sseSession) SetSessionResources(resources map[string]ServerResource) {
|
||||
// Clear existing resources
|
||||
s.resources.Clear()
|
||||
|
||||
// Set new resources
|
||||
for name, resource := range resources {
|
||||
s.resources.Store(name, resource)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sseSession) GetSessionResourceTemplates() map[string]ServerResourceTemplate {
|
||||
templates := make(map[string]ServerResourceTemplate)
|
||||
s.resourceTemplates.Range(func(key, value any) bool {
|
||||
if template, ok := value.(ServerResourceTemplate); ok {
|
||||
templates[key.(string)] = template
|
||||
}
|
||||
return true
|
||||
})
|
||||
return templates
|
||||
}
|
||||
|
||||
func (s *sseSession) SetSessionResourceTemplates(templates map[string]ServerResourceTemplate) {
|
||||
// Clear existing templates
|
||||
s.resourceTemplates.Clear()
|
||||
|
||||
// Set new templates
|
||||
for uriTemplate, template := range templates {
|
||||
s.resourceTemplates.Store(uriTemplate, template)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sseSession) GetSessionTools() map[string]ServerTool {
|
||||
tools := make(map[string]ServerTool)
|
||||
s.tools.Range(func(key, value any) bool {
|
||||
@@ -123,10 +172,12 @@ func (s *sseSession) GetClientCapabilities() mcp.ClientCapabilities {
|
||||
}
|
||||
|
||||
var (
|
||||
_ ClientSession = (*sseSession)(nil)
|
||||
_ SessionWithTools = (*sseSession)(nil)
|
||||
_ SessionWithLogging = (*sseSession)(nil)
|
||||
_ SessionWithClientInfo = (*sseSession)(nil)
|
||||
_ ClientSession = (*sseSession)(nil)
|
||||
_ SessionWithTools = (*sseSession)(nil)
|
||||
_ SessionWithResources = (*sseSession)(nil)
|
||||
_ SessionWithResourceTemplates = (*sseSession)(nil)
|
||||
_ SessionWithLogging = (*sseSession)(nil)
|
||||
_ SessionWithClientInfo = (*sseSession)(nil)
|
||||
)
|
||||
|
||||
// SSEServer implements a Server-Sent Events (SSE) based MCP server.
|
||||
@@ -143,6 +194,7 @@ type SSEServer struct {
|
||||
srv *http.Server
|
||||
contextFunc SSEContextFunc
|
||||
dynamicBasePathFunc DynamicBasePathFunc
|
||||
sessionIDGenFunc SessionIDGenFunc
|
||||
|
||||
keepAlive bool
|
||||
keepAliveInterval time.Duration
|
||||
@@ -271,6 +323,15 @@ func WithSSEContextFunc(fn SSEContextFunc) SSEOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithSessionIDGenerator sets a custom session ID generator. If fn == nil the call is ignored.
|
||||
func WithSessionIDGenerator(fn SessionIDGenFunc) SSEOption {
|
||||
return func(s *SSEServer) {
|
||||
if fn != nil {
|
||||
s.sessionIDGenFunc = fn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewSSEServer creates a new SSE server instance with the given MCP server and options.
|
||||
func NewSSEServer(server *MCPServer, opts ...SSEOption) *SSEServer {
|
||||
s := &SSEServer{
|
||||
@@ -280,6 +341,9 @@ func NewSSEServer(server *MCPServer, opts ...SSEOption) *SSEServer {
|
||||
useFullURLForMessageEndpoint: true,
|
||||
keepAlive: false,
|
||||
keepAliveInterval: 10 * time.Second,
|
||||
sessionIDGenFunc: func(ctx context.Context, r *http.Request) (string, error) {
|
||||
return uuid.New().String(), nil
|
||||
},
|
||||
}
|
||||
|
||||
// Apply all options
|
||||
@@ -361,7 +425,16 @@ func (s *SSEServer) handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
sessionID := uuid.New().String()
|
||||
sessionID, err := s.sessionIDGenFunc(r.Context(), r)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create session ID", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if sessionID == "" {
|
||||
http.Error(w, "Failed to create session ID", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
session := &sseSession{
|
||||
done: make(chan struct{}),
|
||||
eventQueue: make(chan string, 100), // Buffer for events
|
||||
|
||||
319
vendor/github.com/mark3labs/mcp-go/server/stdio.go
generated
vendored
319
vendor/github.com/mark3labs/mcp-go/server/stdio.go
generated
vendored
@@ -92,16 +92,18 @@ func WithQueueSize(size int) StdioOption {
|
||||
|
||||
// stdioSession is a static client session, since stdio has only one client.
|
||||
type stdioSession struct {
|
||||
notifications chan mcp.JSONRPCNotification
|
||||
initialized atomic.Bool
|
||||
loggingLevel atomic.Value
|
||||
clientInfo atomic.Value // stores session-specific client info
|
||||
clientCapabilities atomic.Value // stores session-specific client capabilities
|
||||
writer io.Writer // for sending requests to client
|
||||
requestID atomic.Int64 // for generating unique request IDs
|
||||
mu sync.RWMutex // protects writer
|
||||
pendingRequests map[int64]chan *samplingResponse // for tracking pending sampling requests
|
||||
pendingMu sync.RWMutex // protects pendingRequests
|
||||
notifications chan mcp.JSONRPCNotification
|
||||
initialized atomic.Bool
|
||||
loggingLevel atomic.Value
|
||||
clientInfo atomic.Value // stores session-specific client info
|
||||
clientCapabilities atomic.Value // stores session-specific client capabilities
|
||||
writer io.Writer // for sending requests to client
|
||||
requestID atomic.Int64 // for generating unique request IDs
|
||||
mu sync.RWMutex // protects writer
|
||||
pendingRequests map[int64]chan *samplingResponse // for tracking pending sampling requests
|
||||
pendingElicitations map[int64]chan *elicitationResponse // for tracking pending elicitation requests
|
||||
pendingRoots map[int64]chan *rootsResponse // for tracking pending list roots requests
|
||||
pendingMu sync.RWMutex // protects pendingRequests and pendingElicitations
|
||||
}
|
||||
|
||||
// samplingResponse represents a response to a sampling request
|
||||
@@ -110,6 +112,18 @@ type samplingResponse struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// elicitationResponse represents a response to an elicitation request
|
||||
type elicitationResponse struct {
|
||||
result *mcp.ElicitationResult
|
||||
err error
|
||||
}
|
||||
|
||||
// rootsResponse represents a response to an list root request
|
||||
type rootsResponse struct {
|
||||
result *mcp.ListRootsResult
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *stdioSession) SessionID() string {
|
||||
return "stdio"
|
||||
}
|
||||
@@ -229,6 +243,130 @@ func (s *stdioSession) RequestSampling(ctx context.Context, request mcp.CreateMe
|
||||
}
|
||||
}
|
||||
|
||||
// ListRoots sends an list roots request to the client and waits for the response.
|
||||
func (s *stdioSession) ListRoots(ctx context.Context, request mcp.ListRootsRequest) (*mcp.ListRootsResult, error) {
|
||||
s.mu.RLock()
|
||||
writer := s.writer
|
||||
s.mu.RUnlock()
|
||||
|
||||
if writer == nil {
|
||||
return nil, fmt.Errorf("no writer available for sending requests")
|
||||
}
|
||||
|
||||
// Generate a unique request ID
|
||||
id := s.requestID.Add(1)
|
||||
|
||||
// Create a response channel for this request
|
||||
responseChan := make(chan *rootsResponse, 1)
|
||||
s.pendingMu.Lock()
|
||||
s.pendingRoots[id] = responseChan
|
||||
s.pendingMu.Unlock()
|
||||
|
||||
// Cleanup function to remove the pending request
|
||||
cleanup := func() {
|
||||
s.pendingMu.Lock()
|
||||
delete(s.pendingRoots, id)
|
||||
s.pendingMu.Unlock()
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// Create the JSON-RPC request
|
||||
jsonRPCRequest := struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID int64 `json:"id"`
|
||||
Method string `json:"method"`
|
||||
}{
|
||||
JSONRPC: mcp.JSONRPC_VERSION,
|
||||
ID: id,
|
||||
Method: string(mcp.MethodListRoots),
|
||||
}
|
||||
|
||||
// Marshal and send the request
|
||||
requestBytes, err := json.Marshal(jsonRPCRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal list roots request: %w", err)
|
||||
}
|
||||
requestBytes = append(requestBytes, '\n')
|
||||
|
||||
if _, err := writer.Write(requestBytes); err != nil {
|
||||
return nil, fmt.Errorf("failed to write list roots request: %w", err)
|
||||
}
|
||||
|
||||
// Wait for the response or context cancellation
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case response := <-responseChan:
|
||||
if response.err != nil {
|
||||
return nil, response.err
|
||||
}
|
||||
return response.result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// RequestElicitation sends an elicitation request to the client and waits for the response.
|
||||
func (s *stdioSession) RequestElicitation(ctx context.Context, request mcp.ElicitationRequest) (*mcp.ElicitationResult, error) {
|
||||
s.mu.RLock()
|
||||
writer := s.writer
|
||||
s.mu.RUnlock()
|
||||
|
||||
if writer == nil {
|
||||
return nil, fmt.Errorf("no writer available for sending requests")
|
||||
}
|
||||
|
||||
// Generate a unique request ID
|
||||
id := s.requestID.Add(1)
|
||||
|
||||
// Create a response channel for this request
|
||||
responseChan := make(chan *elicitationResponse, 1)
|
||||
s.pendingMu.Lock()
|
||||
s.pendingElicitations[id] = responseChan
|
||||
s.pendingMu.Unlock()
|
||||
|
||||
// Cleanup function to remove the pending request
|
||||
cleanup := func() {
|
||||
s.pendingMu.Lock()
|
||||
delete(s.pendingElicitations, id)
|
||||
s.pendingMu.Unlock()
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// Create the JSON-RPC request
|
||||
jsonRPCRequest := struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID int64 `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params mcp.ElicitationParams `json:"params"`
|
||||
}{
|
||||
JSONRPC: mcp.JSONRPC_VERSION,
|
||||
ID: id,
|
||||
Method: string(mcp.MethodElicitationCreate),
|
||||
Params: request.Params,
|
||||
}
|
||||
|
||||
// Marshal and send the request
|
||||
requestBytes, err := json.Marshal(jsonRPCRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal elicitation request: %w", err)
|
||||
}
|
||||
requestBytes = append(requestBytes, '\n')
|
||||
|
||||
if _, err := writer.Write(requestBytes); err != nil {
|
||||
return nil, fmt.Errorf("failed to write elicitation request: %w", err)
|
||||
}
|
||||
|
||||
// Wait for the response or context cancellation
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case response := <-responseChan:
|
||||
if response.err != nil {
|
||||
return nil, response.err
|
||||
}
|
||||
return response.result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// SetWriter sets the writer for sending requests to the client.
|
||||
func (s *stdioSession) SetWriter(writer io.Writer) {
|
||||
s.mu.Lock()
|
||||
@@ -237,15 +375,19 @@ func (s *stdioSession) SetWriter(writer io.Writer) {
|
||||
}
|
||||
|
||||
var (
|
||||
_ ClientSession = (*stdioSession)(nil)
|
||||
_ SessionWithLogging = (*stdioSession)(nil)
|
||||
_ SessionWithClientInfo = (*stdioSession)(nil)
|
||||
_ SessionWithSampling = (*stdioSession)(nil)
|
||||
_ ClientSession = (*stdioSession)(nil)
|
||||
_ SessionWithLogging = (*stdioSession)(nil)
|
||||
_ SessionWithClientInfo = (*stdioSession)(nil)
|
||||
_ SessionWithSampling = (*stdioSession)(nil)
|
||||
_ SessionWithElicitation = (*stdioSession)(nil)
|
||||
_ SessionWithRoots = (*stdioSession)(nil)
|
||||
)
|
||||
|
||||
var stdioSessionInstance = stdioSession{
|
||||
notifications: make(chan mcp.JSONRPCNotification, 100),
|
||||
pendingRequests: make(map[int64]chan *samplingResponse),
|
||||
notifications: make(chan mcp.JSONRPCNotification, 100),
|
||||
pendingRequests: make(map[int64]chan *samplingResponse),
|
||||
pendingElicitations: make(map[int64]chan *elicitationResponse),
|
||||
pendingRoots: make(map[int64]chan *rootsResponse),
|
||||
}
|
||||
|
||||
// NewStdioServer creates a new stdio server wrapper around an MCPServer.
|
||||
@@ -445,6 +587,16 @@ func (s *StdioServer) processMessage(
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if this is a response to an elicitation request
|
||||
if s.handleElicitationResponse(rawMessage) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if this is a response to an list roots request
|
||||
if s.handleListRootsResponse(rawMessage) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if this is a tool call that might need sampling (and thus should be processed concurrently)
|
||||
var baseMessage struct {
|
||||
Method string `json:"method"`
|
||||
@@ -529,7 +681,18 @@ func (s *stdioSession) handleSamplingResponse(rawMessage json.RawMessage) bool {
|
||||
if err := json.Unmarshal(response.Result, &result); err != nil {
|
||||
samplingResp.err = fmt.Errorf("failed to unmarshal sampling response: %w", err)
|
||||
} else {
|
||||
samplingResp.result = &result
|
||||
// Parse content from map[string]any to proper Content type (TextContent, ImageContent, AudioContent)
|
||||
if contentMap, ok := result.Content.(map[string]any); ok {
|
||||
content, err := mcp.ParseContent(contentMap)
|
||||
if err != nil {
|
||||
samplingResp.err = fmt.Errorf("failed to parse sampling response content: %w", err)
|
||||
} else {
|
||||
result.Content = content
|
||||
samplingResp.result = &result
|
||||
}
|
||||
} else {
|
||||
samplingResp.result = &result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,6 +706,128 @@ func (s *stdioSession) handleSamplingResponse(rawMessage json.RawMessage) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// handleElicitationResponse checks if the message is a response to an elicitation request
|
||||
// and routes it to the appropriate pending request channel.
|
||||
func (s *StdioServer) handleElicitationResponse(rawMessage json.RawMessage) bool {
|
||||
return stdioSessionInstance.handleElicitationResponse(rawMessage)
|
||||
}
|
||||
|
||||
// handleElicitationResponse handles incoming elicitation responses for this session
|
||||
func (s *stdioSession) handleElicitationResponse(rawMessage json.RawMessage) bool {
|
||||
// Try to parse as a JSON-RPC response
|
||||
var response struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID json.Number `json:"id"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
Error *struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(rawMessage, &response); err != nil {
|
||||
return false
|
||||
}
|
||||
// Parse the ID as int64
|
||||
id, err := response.ID.Int64()
|
||||
if err != nil || (response.Result == nil && response.Error == nil) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if we have a pending elicitation request with this ID
|
||||
s.pendingMu.RLock()
|
||||
responseChan, exists := s.pendingElicitations[id]
|
||||
s.pendingMu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse and send the response
|
||||
elicitationResp := &elicitationResponse{}
|
||||
|
||||
if response.Error != nil {
|
||||
elicitationResp.err = fmt.Errorf("elicitation request failed: %s", response.Error.Message)
|
||||
} else {
|
||||
var result mcp.ElicitationResult
|
||||
if err := json.Unmarshal(response.Result, &result); err != nil {
|
||||
elicitationResp.err = fmt.Errorf("failed to unmarshal elicitation response: %w", err)
|
||||
} else {
|
||||
elicitationResp.result = &result
|
||||
}
|
||||
}
|
||||
|
||||
// Send the response (non-blocking)
|
||||
select {
|
||||
case responseChan <- elicitationResp:
|
||||
default:
|
||||
// Channel is full or closed, ignore
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// handleListRootsResponse checks if the message is a response to an list roots request
|
||||
// and routes it to the appropriate pending request channel.
|
||||
func (s *StdioServer) handleListRootsResponse(rawMessage json.RawMessage) bool {
|
||||
return stdioSessionInstance.handleListRootsResponse(rawMessage)
|
||||
}
|
||||
|
||||
// handleListRootsResponse handles incoming list root responses for this session
|
||||
func (s *stdioSession) handleListRootsResponse(rawMessage json.RawMessage) bool {
|
||||
// Try to parse as a JSON-RPC response
|
||||
var response struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID json.Number `json:"id"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
Error *struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(rawMessage, &response); err != nil {
|
||||
return false
|
||||
}
|
||||
// Parse the ID as int64
|
||||
id, err := response.ID.Int64()
|
||||
if err != nil || (response.Result == nil && response.Error == nil) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if we have a pending list root request with this ID
|
||||
s.pendingMu.RLock()
|
||||
responseChan, exists := s.pendingRoots[id]
|
||||
s.pendingMu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse and send the response
|
||||
rootsResp := &rootsResponse{}
|
||||
|
||||
if response.Error != nil {
|
||||
rootsResp.err = fmt.Errorf("list root request failed: %s", response.Error.Message)
|
||||
} else {
|
||||
var result mcp.ListRootsResult
|
||||
if err := json.Unmarshal(response.Result, &result); err != nil {
|
||||
rootsResp.err = fmt.Errorf("failed to unmarshal list root response: %w", err)
|
||||
} else {
|
||||
rootsResp.result = &result
|
||||
}
|
||||
}
|
||||
|
||||
// Send the response (non-blocking)
|
||||
select {
|
||||
case responseChan <- rootsResp:
|
||||
default:
|
||||
// Channel is full or closed, ignore
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// writeResponse marshals and writes a JSON-RPC response message followed by a newline.
|
||||
// Returns an error if marshaling or writing fails.
|
||||
func (s *StdioServer) writeResponse(
|
||||
|
||||
865
vendor/github.com/mark3labs/mcp-go/server/streamable_http.go
generated
vendored
865
vendor/github.com/mark3labs/mcp-go/server/streamable_http.go
generated
vendored
File diff suppressed because it is too large
Load Diff
136
vendor/github.com/mark3labs/mcp-go/server/task_hooks.go
generated
vendored
Normal file
136
vendor/github.com/mark3labs/mcp-go/server/task_hooks.go
generated
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// TaskMetrics contains metrics about task execution.
|
||||
// This struct is passed to observability hooks to enable monitoring and analysis.
|
||||
type TaskMetrics struct {
|
||||
TaskID string // Unique identifier for the task
|
||||
ToolName string // Name of the tool that created the task
|
||||
Status mcp.TaskStatus // Current status of the task
|
||||
StatusMessage string // Optional status message
|
||||
CreatedAt time.Time // When the task was created
|
||||
CompletedAt *time.Time // When the task completed (nil if not completed)
|
||||
Duration time.Duration // How long the task took (0 if not completed)
|
||||
SessionID string // Session that owns this task
|
||||
Error error // Error if task failed (nil otherwise)
|
||||
}
|
||||
|
||||
// OnTaskCreatedHookFunc is called when a new task is created.
|
||||
// Use this to track task creation metrics, initialize monitoring, or log task starts.
|
||||
type OnTaskCreatedHookFunc func(ctx context.Context, metrics TaskMetrics)
|
||||
|
||||
// OnTaskCompletedHookFunc is called when a task completes successfully.
|
||||
// Use this to track completion metrics, record duration, or trigger follow-up actions.
|
||||
type OnTaskCompletedHookFunc func(ctx context.Context, metrics TaskMetrics)
|
||||
|
||||
// OnTaskFailedHookFunc is called when a task fails with an error.
|
||||
// Use this to track failure metrics, alert on errors, or log failure details.
|
||||
type OnTaskFailedHookFunc func(ctx context.Context, metrics TaskMetrics)
|
||||
|
||||
// OnTaskCancelledHookFunc is called when a task is cancelled.
|
||||
// Use this to track cancellation metrics or clean up resources.
|
||||
type OnTaskCancelledHookFunc func(ctx context.Context, metrics TaskMetrics)
|
||||
|
||||
// OnTaskStatusChangedHookFunc is called whenever a task's status changes.
|
||||
// This is a catch-all hook that fires for all status transitions.
|
||||
// Use this for general monitoring or when you need to track all state changes.
|
||||
type OnTaskStatusChangedHookFunc func(ctx context.Context, metrics TaskMetrics)
|
||||
|
||||
// TaskHooks contains lifecycle hooks for task execution.
|
||||
// These hooks enable observability and monitoring of task-augmented tools.
|
||||
type TaskHooks struct {
|
||||
OnTaskCreated []OnTaskCreatedHookFunc
|
||||
OnTaskCompleted []OnTaskCompletedHookFunc
|
||||
OnTaskFailed []OnTaskFailedHookFunc
|
||||
OnTaskCancelled []OnTaskCancelledHookFunc
|
||||
OnTaskStatusChanged []OnTaskStatusChangedHookFunc
|
||||
}
|
||||
|
||||
// AddOnTaskCreated registers a hook for task creation events.
|
||||
func (h *TaskHooks) AddOnTaskCreated(hook OnTaskCreatedHookFunc) {
|
||||
h.OnTaskCreated = append(h.OnTaskCreated, hook)
|
||||
}
|
||||
|
||||
// AddOnTaskCompleted registers a hook for task completion events.
|
||||
func (h *TaskHooks) AddOnTaskCompleted(hook OnTaskCompletedHookFunc) {
|
||||
h.OnTaskCompleted = append(h.OnTaskCompleted, hook)
|
||||
}
|
||||
|
||||
// AddOnTaskFailed registers a hook for task failure events.
|
||||
func (h *TaskHooks) AddOnTaskFailed(hook OnTaskFailedHookFunc) {
|
||||
h.OnTaskFailed = append(h.OnTaskFailed, hook)
|
||||
}
|
||||
|
||||
// AddOnTaskCancelled registers a hook for task cancellation events.
|
||||
func (h *TaskHooks) AddOnTaskCancelled(hook OnTaskCancelledHookFunc) {
|
||||
h.OnTaskCancelled = append(h.OnTaskCancelled, hook)
|
||||
}
|
||||
|
||||
// AddOnTaskStatusChanged registers a hook for all task status changes.
|
||||
func (h *TaskHooks) AddOnTaskStatusChanged(hook OnTaskStatusChangedHookFunc) {
|
||||
h.OnTaskStatusChanged = append(h.OnTaskStatusChanged, hook)
|
||||
}
|
||||
|
||||
// taskCreated calls all registered task creation hooks.
|
||||
func (h *TaskHooks) taskCreated(ctx context.Context, metrics TaskMetrics) {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
for _, hook := range h.OnTaskCreated {
|
||||
hook(ctx, metrics)
|
||||
}
|
||||
// Also call status changed hook
|
||||
h.taskStatusChanged(ctx, metrics)
|
||||
}
|
||||
|
||||
// taskCompleted calls all registered task completion hooks.
|
||||
func (h *TaskHooks) taskCompleted(ctx context.Context, metrics TaskMetrics) {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
for _, hook := range h.OnTaskCompleted {
|
||||
hook(ctx, metrics)
|
||||
}
|
||||
// Also call status changed hook
|
||||
h.taskStatusChanged(ctx, metrics)
|
||||
}
|
||||
|
||||
// taskFailed calls all registered task failure hooks.
|
||||
func (h *TaskHooks) taskFailed(ctx context.Context, metrics TaskMetrics) {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
for _, hook := range h.OnTaskFailed {
|
||||
hook(ctx, metrics)
|
||||
}
|
||||
// Also call status changed hook
|
||||
h.taskStatusChanged(ctx, metrics)
|
||||
}
|
||||
|
||||
// taskCancelled calls all registered task cancellation hooks.
|
||||
func (h *TaskHooks) taskCancelled(ctx context.Context, metrics TaskMetrics) {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
for _, hook := range h.OnTaskCancelled {
|
||||
hook(ctx, metrics)
|
||||
}
|
||||
// Also call status changed hook
|
||||
h.taskStatusChanged(ctx, metrics)
|
||||
}
|
||||
|
||||
// taskStatusChanged calls all registered status change hooks.
|
||||
func (h *TaskHooks) taskStatusChanged(ctx context.Context, metrics TaskMetrics) {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
for _, hook := range h.OnTaskStatusChanged {
|
||||
hook(ctx, metrics)
|
||||
}
|
||||
}
|
||||
1
vendor/github.com/wk8/go-ordered-map/v2/.gitignore
generated
vendored
1
vendor/github.com/wk8/go-ordered-map/v2/.gitignore
generated
vendored
@@ -1 +0,0 @@
|
||||
/vendor/
|
||||
80
vendor/github.com/wk8/go-ordered-map/v2/.golangci.yml
generated
vendored
80
vendor/github.com/wk8/go-ordered-map/v2/.golangci.yml
generated
vendored
@@ -1,80 +0,0 @@
|
||||
run:
|
||||
tests: false
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- containedctx
|
||||
- contextcheck
|
||||
- decorder
|
||||
- depguard
|
||||
- dogsled
|
||||
- dupl
|
||||
- durationcheck
|
||||
- errcheck
|
||||
- errchkjson
|
||||
# FIXME: commented out as it crashes with 1.18 for now
|
||||
# - errname
|
||||
- errorlint
|
||||
- exportloopref
|
||||
- forbidigo
|
||||
- funlen
|
||||
- gci
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godox
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- goheader
|
||||
- goimports
|
||||
- gomnd
|
||||
- gomoddirectives
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- grouper
|
||||
- ifshort
|
||||
- importas
|
||||
- ineffassign
|
||||
- lll
|
||||
- maintidx
|
||||
- makezero
|
||||
- misspell
|
||||
- nakedret
|
||||
- nilerr
|
||||
- nilnil
|
||||
- noctx
|
||||
- nolintlint
|
||||
- paralleltest
|
||||
- prealloc
|
||||
- predeclared
|
||||
- promlinter
|
||||
# FIXME: doesn't support 1.18 yet
|
||||
# - revive
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- stylecheck
|
||||
- tagliatelle
|
||||
- tenv
|
||||
- testpackage
|
||||
- thelper
|
||||
- tparallel
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- varnamelen
|
||||
- wastedassign
|
||||
- whitespace
|
||||
38
vendor/github.com/wk8/go-ordered-map/v2/CHANGELOG.md
generated
vendored
38
vendor/github.com/wk8/go-ordered-map/v2/CHANGELOG.md
generated
vendored
@@ -1,38 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
[comment]: # (Changes since last release go here)
|
||||
|
||||
## 2.1.8 - Jun 27th 2023
|
||||
|
||||
* Added support for YAML serialization/deserialization
|
||||
|
||||
## 2.1.7 - Apr 13th 2023
|
||||
|
||||
* Renamed test_utils.go to utils_test.go
|
||||
|
||||
## 2.1.6 - Feb 15th 2023
|
||||
|
||||
* Added `GetAndMoveToBack()` and `GetAndMoveToFront()` methods
|
||||
|
||||
## 2.1.5 - Dec 13th 2022
|
||||
|
||||
* Added `Value()` method
|
||||
|
||||
## 2.1.4 - Dec 12th 2022
|
||||
|
||||
* Fixed a bug with UTF-8 special characters in JSON keys
|
||||
|
||||
## 2.1.3 - Dec 11th 2022
|
||||
|
||||
* Added support for JSON marshalling/unmarshalling of wrapper of primitive types
|
||||
|
||||
## 2.1.2 - Dec 10th 2022
|
||||
* Allowing to pass options to `New`, to give a capacity hint, or initial data
|
||||
* Allowing to deserialize nested ordered maps from JSON without having to explicitly instantiate them
|
||||
* Added the `AddPairs` method
|
||||
|
||||
## 2.1.1 - Dec 9th 2022
|
||||
* Fixing a bug with JSON marshalling
|
||||
|
||||
## 2.1.0 - Dec 7th 2022
|
||||
* Added support for JSON serialization/deserialization
|
||||
201
vendor/github.com/wk8/go-ordered-map/v2/LICENSE
generated
vendored
201
vendor/github.com/wk8/go-ordered-map/v2/LICENSE
generated
vendored
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
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.
|
||||
32
vendor/github.com/wk8/go-ordered-map/v2/Makefile
generated
vendored
32
vendor/github.com/wk8/go-ordered-map/v2/Makefile
generated
vendored
@@ -1,32 +0,0 @@
|
||||
.DEFAULT_GOAL := all
|
||||
|
||||
.PHONY: all
|
||||
all: test_with_fuzz lint
|
||||
|
||||
# the TEST_FLAGS env var can be set to eg run only specific tests
|
||||
TEST_COMMAND = go test -v -count=1 -race -cover $(TEST_FLAGS)
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
$(TEST_COMMAND)
|
||||
|
||||
.PHONY: bench
|
||||
bench:
|
||||
go test -bench=.
|
||||
|
||||
FUZZ_TIME ?= 10s
|
||||
|
||||
# see https://github.com/golang/go/issues/46312
|
||||
# and https://stackoverflow.com/a/72673487/4867444
|
||||
# if we end up having more fuzz tests
|
||||
.PHONY: test_with_fuzz
|
||||
test_with_fuzz:
|
||||
$(TEST_COMMAND) -fuzz=FuzzRoundTripJSON -fuzztime=$(FUZZ_TIME)
|
||||
$(TEST_COMMAND) -fuzz=FuzzRoundTripYAML -fuzztime=$(FUZZ_TIME)
|
||||
|
||||
.PHONY: fuzz
|
||||
fuzz: test_with_fuzz
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
golangci-lint run
|
||||
154
vendor/github.com/wk8/go-ordered-map/v2/README.md
generated
vendored
154
vendor/github.com/wk8/go-ordered-map/v2/README.md
generated
vendored
@@ -1,154 +0,0 @@
|
||||
[](https://pkg.go.dev/github.com/wk8/go-ordered-map/v2)
|
||||
[](https://app.circleci.com/pipelines/github/wk8/go-ordered-map)
|
||||
|
||||
# Golang Ordered Maps
|
||||
|
||||
Same as regular maps, but also remembers the order in which keys were inserted, akin to [Python's `collections.OrderedDict`s](https://docs.python.org/3.7/library/collections.html#ordereddict-objects).
|
||||
|
||||
It offers the following features:
|
||||
* optimal runtime performance (all operations are constant time)
|
||||
* optimal memory usage (only one copy of values, no unnecessary memory allocation)
|
||||
* allows iterating from newest or oldest keys indifferently, without memory copy, allowing to `break` the iteration, and in time linear to the number of keys iterated over rather than the total length of the ordered map
|
||||
* supports any generic types for both keys and values. If you're running go < 1.18, you can use [version 1](https://github.com/wk8/go-ordered-map/tree/v1) that takes and returns generic `interface{}`s instead of using generics
|
||||
* idiomatic API, akin to that of [`container/list`](https://golang.org/pkg/container/list)
|
||||
* support for JSON and YAML marshalling
|
||||
|
||||
## Documentation
|
||||
|
||||
[The full documentation is available on pkg.go.dev](https://pkg.go.dev/github.com/wk8/go-ordered-map/v2).
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
go get -u github.com/wk8/go-ordered-map/v2
|
||||
```
|
||||
|
||||
Or use your favorite golang vendoring tool!
|
||||
|
||||
## Supported go versions
|
||||
|
||||
Go >= 1.18 is required to use version >= 2 of this library, as it uses generics.
|
||||
|
||||
If you're running go < 1.18, you can use [version 1](https://github.com/wk8/go-ordered-map/tree/v1) instead.
|
||||
|
||||
## Example / usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wk8/go-ordered-map/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
om := orderedmap.New[string, string]()
|
||||
|
||||
om.Set("foo", "bar")
|
||||
om.Set("bar", "baz")
|
||||
om.Set("coucou", "toi")
|
||||
|
||||
fmt.Println(om.Get("foo")) // => "bar", true
|
||||
fmt.Println(om.Get("i dont exist")) // => "", false
|
||||
|
||||
// iterating pairs from oldest to newest:
|
||||
for pair := om.Oldest(); pair != nil; pair = pair.Next() {
|
||||
fmt.Printf("%s => %s\n", pair.Key, pair.Value)
|
||||
} // prints:
|
||||
// foo => bar
|
||||
// bar => baz
|
||||
// coucou => toi
|
||||
|
||||
// iterating over the 2 newest pairs:
|
||||
i := 0
|
||||
for pair := om.Newest(); pair != nil; pair = pair.Prev() {
|
||||
fmt.Printf("%s => %s\n", pair.Key, pair.Value)
|
||||
i++
|
||||
if i >= 2 {
|
||||
break
|
||||
}
|
||||
} // prints:
|
||||
// coucou => toi
|
||||
// bar => baz
|
||||
}
|
||||
```
|
||||
|
||||
An `OrderedMap`'s keys must implement `comparable`, and its values can be anything, for example:
|
||||
|
||||
```go
|
||||
type myStruct struct {
|
||||
payload string
|
||||
}
|
||||
|
||||
func main() {
|
||||
om := orderedmap.New[int, *myStruct]()
|
||||
|
||||
om.Set(12, &myStruct{"foo"})
|
||||
om.Set(1, &myStruct{"bar"})
|
||||
|
||||
value, present := om.Get(12)
|
||||
if !present {
|
||||
panic("should be there!")
|
||||
}
|
||||
fmt.Println(value.payload) // => foo
|
||||
|
||||
for pair := om.Oldest(); pair != nil; pair = pair.Next() {
|
||||
fmt.Printf("%d => %s\n", pair.Key, pair.Value.payload)
|
||||
} // prints:
|
||||
// 12 => foo
|
||||
// 1 => bar
|
||||
}
|
||||
```
|
||||
|
||||
Also worth noting that you can provision ordered maps with a capacity hint, as you would do by passing an optional hint to `make(map[K]V, capacity`):
|
||||
```go
|
||||
om := orderedmap.New[int, *myStruct](28)
|
||||
```
|
||||
|
||||
You can also pass in some initial data to store in the map:
|
||||
```go
|
||||
om := orderedmap.New[int, string](orderedmap.WithInitialData[int, string](
|
||||
orderedmap.Pair[int, string]{
|
||||
Key: 12,
|
||||
Value: "foo",
|
||||
},
|
||||
orderedmap.Pair[int, string]{
|
||||
Key: 28,
|
||||
Value: "bar",
|
||||
},
|
||||
))
|
||||
```
|
||||
|
||||
`OrderedMap`s also support JSON serialization/deserialization, and preserves order:
|
||||
|
||||
```go
|
||||
// serialization
|
||||
data, err := json.Marshal(om)
|
||||
...
|
||||
|
||||
// deserialization
|
||||
om := orderedmap.New[string, string]() // or orderedmap.New[int, any](), or any type you expect
|
||||
err := json.Unmarshal(data, &om)
|
||||
...
|
||||
```
|
||||
|
||||
Similarly, it also supports YAML serialization/deserialization using the yaml.v3 package, which also preserves order:
|
||||
|
||||
```go
|
||||
// serialization
|
||||
data, err := yaml.Marshal(om)
|
||||
...
|
||||
|
||||
// deserialization
|
||||
om := orderedmap.New[string, string]() // or orderedmap.New[int, any](), or any type you expect
|
||||
err := yaml.Unmarshal(data, &om)
|
||||
...
|
||||
```
|
||||
|
||||
## Alternatives
|
||||
|
||||
There are several other ordered map golang implementations out there, but I believe that at the time of writing none of them offer the same functionality as this library; more specifically:
|
||||
* [iancoleman/orderedmap](https://github.com/iancoleman/orderedmap) only accepts `string` keys, its `Delete` operations are linear
|
||||
* [cevaris/ordered_map](https://github.com/cevaris/ordered_map) uses a channel for iterations, and leaks goroutines if the iteration is interrupted before fully traversing the map
|
||||
* [mantyr/iterator](https://github.com/mantyr/iterator) also uses a channel for iterations, and its `Delete` operations are linear
|
||||
* [samdolan/go-ordered-map](https://github.com/samdolan/go-ordered-map) adds unnecessary locking (users should add their own locking instead if they need it), its `Delete` and `Get` operations are linear, iterations trigger a linear memory allocation
|
||||
182
vendor/github.com/wk8/go-ordered-map/v2/json.go
generated
vendored
182
vendor/github.com/wk8/go-ordered-map/v2/json.go
generated
vendored
@@ -1,182 +0,0 @@
|
||||
package orderedmap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/buger/jsonparser"
|
||||
"github.com/mailru/easyjson/jwriter"
|
||||
)
|
||||
|
||||
var (
|
||||
_ json.Marshaler = &OrderedMap[int, any]{}
|
||||
_ json.Unmarshaler = &OrderedMap[int, any]{}
|
||||
)
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
func (om *OrderedMap[K, V]) MarshalJSON() ([]byte, error) { //nolint:funlen
|
||||
if om == nil || om.list == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
|
||||
writer := jwriter.Writer{}
|
||||
writer.RawByte('{')
|
||||
|
||||
for pair, firstIteration := om.Oldest(), true; pair != nil; pair = pair.Next() {
|
||||
if firstIteration {
|
||||
firstIteration = false
|
||||
} else {
|
||||
writer.RawByte(',')
|
||||
}
|
||||
|
||||
switch key := any(pair.Key).(type) {
|
||||
case string:
|
||||
writer.String(key)
|
||||
case encoding.TextMarshaler:
|
||||
writer.RawByte('"')
|
||||
writer.Raw(key.MarshalText())
|
||||
writer.RawByte('"')
|
||||
case int:
|
||||
writer.IntStr(key)
|
||||
case int8:
|
||||
writer.Int8Str(key)
|
||||
case int16:
|
||||
writer.Int16Str(key)
|
||||
case int32:
|
||||
writer.Int32Str(key)
|
||||
case int64:
|
||||
writer.Int64Str(key)
|
||||
case uint:
|
||||
writer.UintStr(key)
|
||||
case uint8:
|
||||
writer.Uint8Str(key)
|
||||
case uint16:
|
||||
writer.Uint16Str(key)
|
||||
case uint32:
|
||||
writer.Uint32Str(key)
|
||||
case uint64:
|
||||
writer.Uint64Str(key)
|
||||
default:
|
||||
|
||||
// this switch takes care of wrapper types around primitive types, such as
|
||||
// type myType string
|
||||
switch keyValue := reflect.ValueOf(key); keyValue.Type().Kind() {
|
||||
case reflect.String:
|
||||
writer.String(keyValue.String())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
writer.Int64Str(keyValue.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
writer.Uint64Str(keyValue.Uint())
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported key type: %T", key)
|
||||
}
|
||||
}
|
||||
|
||||
writer.RawByte(':')
|
||||
// the error is checked at the end of the function
|
||||
writer.Raw(json.Marshal(pair.Value)) //nolint:errchkjson
|
||||
}
|
||||
|
||||
writer.RawByte('}')
|
||||
|
||||
return dumpWriter(&writer)
|
||||
}
|
||||
|
||||
func dumpWriter(writer *jwriter.Writer) ([]byte, error) {
|
||||
if writer.Error != nil {
|
||||
return nil, writer.Error
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.Grow(writer.Size())
|
||||
if _, err := writer.DumpTo(&buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (om *OrderedMap[K, V]) UnmarshalJSON(data []byte) error {
|
||||
if om.list == nil {
|
||||
om.initialize(0)
|
||||
}
|
||||
|
||||
return jsonparser.ObjectEach(
|
||||
data,
|
||||
func(keyData []byte, valueData []byte, dataType jsonparser.ValueType, offset int) error {
|
||||
if dataType == jsonparser.String {
|
||||
// jsonparser removes the enclosing quotes; we need to restore them to make a valid JSON
|
||||
valueData = data[offset-len(valueData)-2 : offset]
|
||||
}
|
||||
|
||||
var key K
|
||||
var value V
|
||||
|
||||
switch typedKey := any(&key).(type) {
|
||||
case *string:
|
||||
s, err := decodeUTF8(keyData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*typedKey = s
|
||||
case encoding.TextUnmarshaler:
|
||||
if err := typedKey.UnmarshalText(keyData); err != nil {
|
||||
return err
|
||||
}
|
||||
case *int, *int8, *int16, *int32, *int64, *uint, *uint8, *uint16, *uint32, *uint64:
|
||||
if err := json.Unmarshal(keyData, typedKey); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// this switch takes care of wrapper types around primitive types, such as
|
||||
// type myType string
|
||||
switch reflect.TypeOf(key).Kind() {
|
||||
case reflect.String:
|
||||
s, err := decodeUTF8(keyData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
convertedKeyData := reflect.ValueOf(s).Convert(reflect.TypeOf(key))
|
||||
reflect.ValueOf(&key).Elem().Set(convertedKeyData)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if err := json.Unmarshal(keyData, &key); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported key type: %T", key)
|
||||
}
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(valueData, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
om.Set(key, value)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func decodeUTF8(input []byte) (string, error) {
|
||||
remaining, offset := input, 0
|
||||
runes := make([]rune, 0, len(remaining))
|
||||
|
||||
for len(remaining) > 0 {
|
||||
r, size := utf8.DecodeRune(remaining)
|
||||
if r == utf8.RuneError && size <= 1 {
|
||||
return "", fmt.Errorf("not a valid UTF-8 string (at position %d): %s", offset, string(input))
|
||||
}
|
||||
|
||||
runes = append(runes, r)
|
||||
remaining = remaining[size:]
|
||||
offset += size
|
||||
}
|
||||
|
||||
return string(runes), nil
|
||||
}
|
||||
296
vendor/github.com/wk8/go-ordered-map/v2/orderedmap.go
generated
vendored
296
vendor/github.com/wk8/go-ordered-map/v2/orderedmap.go
generated
vendored
@@ -1,296 +0,0 @@
|
||||
// Package orderedmap implements an ordered map, i.e. a map that also keeps track of
|
||||
// the order in which keys were inserted.
|
||||
//
|
||||
// All operations are constant-time.
|
||||
//
|
||||
// Github repo: https://github.com/wk8/go-ordered-map
|
||||
//
|
||||
package orderedmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
list "github.com/bahlo/generic-list-go"
|
||||
)
|
||||
|
||||
type Pair[K comparable, V any] struct {
|
||||
Key K
|
||||
Value V
|
||||
|
||||
element *list.Element[*Pair[K, V]]
|
||||
}
|
||||
|
||||
type OrderedMap[K comparable, V any] struct {
|
||||
pairs map[K]*Pair[K, V]
|
||||
list *list.List[*Pair[K, V]]
|
||||
}
|
||||
|
||||
type initConfig[K comparable, V any] struct {
|
||||
capacity int
|
||||
initialData []Pair[K, V]
|
||||
}
|
||||
|
||||
type InitOption[K comparable, V any] func(config *initConfig[K, V])
|
||||
|
||||
// WithCapacity allows giving a capacity hint for the map, akin to the standard make(map[K]V, capacity).
|
||||
func WithCapacity[K comparable, V any](capacity int) InitOption[K, V] {
|
||||
return func(c *initConfig[K, V]) {
|
||||
c.capacity = capacity
|
||||
}
|
||||
}
|
||||
|
||||
// WithInitialData allows passing in initial data for the map.
|
||||
func WithInitialData[K comparable, V any](initialData ...Pair[K, V]) InitOption[K, V] {
|
||||
return func(c *initConfig[K, V]) {
|
||||
c.initialData = initialData
|
||||
if c.capacity < len(initialData) {
|
||||
c.capacity = len(initialData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new OrderedMap.
|
||||
// options can either be one or several InitOption[K, V], or a single integer,
|
||||
// which is then interpreted as a capacity hint, à la make(map[K]V, capacity).
|
||||
func New[K comparable, V any](options ...any) *OrderedMap[K, V] { //nolint:varnamelen
|
||||
orderedMap := &OrderedMap[K, V]{}
|
||||
|
||||
var config initConfig[K, V]
|
||||
for _, untypedOption := range options {
|
||||
switch option := untypedOption.(type) {
|
||||
case int:
|
||||
if len(options) != 1 {
|
||||
invalidOption()
|
||||
}
|
||||
config.capacity = option
|
||||
|
||||
case InitOption[K, V]:
|
||||
option(&config)
|
||||
|
||||
default:
|
||||
invalidOption()
|
||||
}
|
||||
}
|
||||
|
||||
orderedMap.initialize(config.capacity)
|
||||
orderedMap.AddPairs(config.initialData...)
|
||||
|
||||
return orderedMap
|
||||
}
|
||||
|
||||
const invalidOptionMessage = `when using orderedmap.New[K,V]() with options, either provide one or several InitOption[K, V]; or a single integer which is then interpreted as a capacity hint, à la make(map[K]V, capacity).` //nolint:lll
|
||||
|
||||
func invalidOption() { panic(invalidOptionMessage) }
|
||||
|
||||
func (om *OrderedMap[K, V]) initialize(capacity int) {
|
||||
om.pairs = make(map[K]*Pair[K, V], capacity)
|
||||
om.list = list.New[*Pair[K, V]]()
|
||||
}
|
||||
|
||||
// Get looks for the given key, and returns the value associated with it,
|
||||
// or V's nil value if not found. The boolean it returns says whether the key is present in the map.
|
||||
func (om *OrderedMap[K, V]) Get(key K) (val V, present bool) {
|
||||
if pair, present := om.pairs[key]; present {
|
||||
return pair.Value, true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Load is an alias for Get, mostly to present an API similar to `sync.Map`'s.
|
||||
func (om *OrderedMap[K, V]) Load(key K) (V, bool) {
|
||||
return om.Get(key)
|
||||
}
|
||||
|
||||
// Value returns the value associated with the given key or the zero value.
|
||||
func (om *OrderedMap[K, V]) Value(key K) (val V) {
|
||||
if pair, present := om.pairs[key]; present {
|
||||
val = pair.Value
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetPair looks for the given key, and returns the pair associated with it,
|
||||
// or nil if not found. The Pair struct can then be used to iterate over the ordered map
|
||||
// from that point, either forward or backward.
|
||||
func (om *OrderedMap[K, V]) GetPair(key K) *Pair[K, V] {
|
||||
return om.pairs[key]
|
||||
}
|
||||
|
||||
// Set sets the key-value pair, and returns what `Get` would have returned
|
||||
// on that key prior to the call to `Set`.
|
||||
func (om *OrderedMap[K, V]) Set(key K, value V) (val V, present bool) {
|
||||
if pair, present := om.pairs[key]; present {
|
||||
oldValue := pair.Value
|
||||
pair.Value = value
|
||||
return oldValue, true
|
||||
}
|
||||
|
||||
pair := &Pair[K, V]{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
pair.element = om.list.PushBack(pair)
|
||||
om.pairs[key] = pair
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// AddPairs allows setting multiple pairs at a time. It's equivalent to calling
|
||||
// Set on each pair sequentially.
|
||||
func (om *OrderedMap[K, V]) AddPairs(pairs ...Pair[K, V]) {
|
||||
for _, pair := range pairs {
|
||||
om.Set(pair.Key, pair.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// Store is an alias for Set, mostly to present an API similar to `sync.Map`'s.
|
||||
func (om *OrderedMap[K, V]) Store(key K, value V) (V, bool) {
|
||||
return om.Set(key, value)
|
||||
}
|
||||
|
||||
// Delete removes the key-value pair, and returns what `Get` would have returned
|
||||
// on that key prior to the call to `Delete`.
|
||||
func (om *OrderedMap[K, V]) Delete(key K) (val V, present bool) {
|
||||
if pair, present := om.pairs[key]; present {
|
||||
om.list.Remove(pair.element)
|
||||
delete(om.pairs, key)
|
||||
return pair.Value, true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Len returns the length of the ordered map.
|
||||
func (om *OrderedMap[K, V]) Len() int {
|
||||
if om == nil || om.pairs == nil {
|
||||
return 0
|
||||
}
|
||||
return len(om.pairs)
|
||||
}
|
||||
|
||||
// Oldest returns a pointer to the oldest pair. It's meant to be used to iterate on the ordered map's
|
||||
// pairs from the oldest to the newest, e.g.:
|
||||
// for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() { fmt.Printf("%v => %v\n", pair.Key, pair.Value) }
|
||||
func (om *OrderedMap[K, V]) Oldest() *Pair[K, V] {
|
||||
if om == nil || om.list == nil {
|
||||
return nil
|
||||
}
|
||||
return listElementToPair(om.list.Front())
|
||||
}
|
||||
|
||||
// Newest returns a pointer to the newest pair. It's meant to be used to iterate on the ordered map's
|
||||
// pairs from the newest to the oldest, e.g.:
|
||||
// for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() { fmt.Printf("%v => %v\n", pair.Key, pair.Value) }
|
||||
func (om *OrderedMap[K, V]) Newest() *Pair[K, V] {
|
||||
if om == nil || om.list == nil {
|
||||
return nil
|
||||
}
|
||||
return listElementToPair(om.list.Back())
|
||||
}
|
||||
|
||||
// Next returns a pointer to the next pair.
|
||||
func (p *Pair[K, V]) Next() *Pair[K, V] {
|
||||
return listElementToPair(p.element.Next())
|
||||
}
|
||||
|
||||
// Prev returns a pointer to the previous pair.
|
||||
func (p *Pair[K, V]) Prev() *Pair[K, V] {
|
||||
return listElementToPair(p.element.Prev())
|
||||
}
|
||||
|
||||
func listElementToPair[K comparable, V any](element *list.Element[*Pair[K, V]]) *Pair[K, V] {
|
||||
if element == nil {
|
||||
return nil
|
||||
}
|
||||
return element.Value
|
||||
}
|
||||
|
||||
// KeyNotFoundError may be returned by functions in this package when they're called with keys that are not present
|
||||
// in the map.
|
||||
type KeyNotFoundError[K comparable] struct {
|
||||
MissingKey K
|
||||
}
|
||||
|
||||
func (e *KeyNotFoundError[K]) Error() string {
|
||||
return fmt.Sprintf("missing key: %v", e.MissingKey)
|
||||
}
|
||||
|
||||
// MoveAfter moves the value associated with key to its new position after the one associated with markKey.
|
||||
// Returns an error iff key or markKey are not present in the map. If an error is returned,
|
||||
// it will be a KeyNotFoundError.
|
||||
func (om *OrderedMap[K, V]) MoveAfter(key, markKey K) error {
|
||||
elements, err := om.getElements(key, markKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
om.list.MoveAfter(elements[0], elements[1])
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveBefore moves the value associated with key to its new position before the one associated with markKey.
|
||||
// Returns an error iff key or markKey are not present in the map. If an error is returned,
|
||||
// it will be a KeyNotFoundError.
|
||||
func (om *OrderedMap[K, V]) MoveBefore(key, markKey K) error {
|
||||
elements, err := om.getElements(key, markKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
om.list.MoveBefore(elements[0], elements[1])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (om *OrderedMap[K, V]) getElements(keys ...K) ([]*list.Element[*Pair[K, V]], error) {
|
||||
elements := make([]*list.Element[*Pair[K, V]], len(keys))
|
||||
for i, k := range keys {
|
||||
pair, present := om.pairs[k]
|
||||
if !present {
|
||||
return nil, &KeyNotFoundError[K]{k}
|
||||
}
|
||||
elements[i] = pair.element
|
||||
}
|
||||
return elements, nil
|
||||
}
|
||||
|
||||
// MoveToBack moves the value associated with key to the back of the ordered map,
|
||||
// i.e. makes it the newest pair in the map.
|
||||
// Returns an error iff key is not present in the map. If an error is returned,
|
||||
// it will be a KeyNotFoundError.
|
||||
func (om *OrderedMap[K, V]) MoveToBack(key K) error {
|
||||
_, err := om.GetAndMoveToBack(key)
|
||||
return err
|
||||
}
|
||||
|
||||
// MoveToFront moves the value associated with key to the front of the ordered map,
|
||||
// i.e. makes it the oldest pair in the map.
|
||||
// Returns an error iff key is not present in the map. If an error is returned,
|
||||
// it will be a KeyNotFoundError.
|
||||
func (om *OrderedMap[K, V]) MoveToFront(key K) error {
|
||||
_, err := om.GetAndMoveToFront(key)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetAndMoveToBack combines Get and MoveToBack in the same call. If an error is returned,
|
||||
// it will be a KeyNotFoundError.
|
||||
func (om *OrderedMap[K, V]) GetAndMoveToBack(key K) (val V, err error) {
|
||||
if pair, present := om.pairs[key]; present {
|
||||
val = pair.Value
|
||||
om.list.MoveToBack(pair.element)
|
||||
} else {
|
||||
err = &KeyNotFoundError[K]{key}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetAndMoveToFront combines Get and MoveToFront in the same call. If an error is returned,
|
||||
// it will be a KeyNotFoundError.
|
||||
func (om *OrderedMap[K, V]) GetAndMoveToFront(key K) (val V, err error) {
|
||||
if pair, present := om.pairs[key]; present {
|
||||
val = pair.Value
|
||||
om.list.MoveToFront(pair.element)
|
||||
} else {
|
||||
err = &KeyNotFoundError[K]{key}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
71
vendor/github.com/wk8/go-ordered-map/v2/yaml.go
generated
vendored
71
vendor/github.com/wk8/go-ordered-map/v2/yaml.go
generated
vendored
@@ -1,71 +0,0 @@
|
||||
package orderedmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
_ yaml.Marshaler = &OrderedMap[int, any]{}
|
||||
_ yaml.Unmarshaler = &OrderedMap[int, any]{}
|
||||
)
|
||||
|
||||
// MarshalYAML implements the yaml.Marshaler interface.
|
||||
func (om *OrderedMap[K, V]) MarshalYAML() (interface{}, error) {
|
||||
if om == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
|
||||
node := yaml.Node{
|
||||
Kind: yaml.MappingNode,
|
||||
}
|
||||
|
||||
for pair := om.Oldest(); pair != nil; pair = pair.Next() {
|
||||
key, value := pair.Key, pair.Value
|
||||
|
||||
keyNode := &yaml.Node{}
|
||||
|
||||
// serialize key to yaml, then deserialize it back into the node
|
||||
// this is a hack to get the correct tag for the key
|
||||
if err := keyNode.Encode(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
valueNode := &yaml.Node{}
|
||||
if err := valueNode.Encode(value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
node.Content = append(node.Content, keyNode, valueNode)
|
||||
}
|
||||
|
||||
return &node, nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
||||
func (om *OrderedMap[K, V]) UnmarshalYAML(value *yaml.Node) error {
|
||||
if value.Kind != yaml.MappingNode {
|
||||
return fmt.Errorf("pipeline must contain YAML mapping, has %v", value.Kind)
|
||||
}
|
||||
|
||||
if om.list == nil {
|
||||
om.initialize(0)
|
||||
}
|
||||
|
||||
for index := 0; index < len(value.Content); index += 2 {
|
||||
var key K
|
||||
var val V
|
||||
|
||||
if err := value.Content[index].Decode(&key); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := value.Content[index+1].Decode(&val); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
om.Set(key, val)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user