Files
wehub-resource-sync 4cddfcf2f3
Backend Tests / Tests (other) (push) Failing after 1s
Backend Tests / Static Checks (push) Failing after 2s
Backend Tests / Tests (internal) (push) Failing after 2s
Backend Tests / Tests (server) (push) Failing after 1s
Backend Tests / Tests (store) (push) Failing after 1s
Build Canary Image / build-frontend (push) Failing after 1s
Frontend Tests / Lint (push) Failing after 1s
Build Canary Image / build-push (linux/amd64) (push) Has been skipped
Build Canary Image / build-push (linux/arm64) (push) Has been skipped
Build Canary Image / merge (push) Has been skipped
Frontend Tests / Build (push) Failing after 1s
Release Please / release-please (push) Failing after 0s
Proto Linter / Lint Protos (push) Failing after 2s
chore: import upstream snapshot with attribution
2026-07-13 12:02:24 +08:00

253 lines
7.2 KiB
Go

package mcp
import (
"os"
"strings"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
type jsonSchema map[string]any
type openAPISpec struct {
OpenAPI string `yaml:"openapi"`
Paths map[string]map[string]*openAPIOperation `yaml:"paths"`
Components openAPIComponents `yaml:"components"`
}
type openAPIComponents struct {
Schemas map[string]jsonSchema `yaml:"schemas"`
}
type openAPIOperation struct {
OperationID string `yaml:"operationId"`
Description string `yaml:"description"`
Parameters []openAPIParameter `yaml:"parameters"`
RequestBody *openAPIRequestBody `yaml:"requestBody"`
Responses map[string]openAPIResponse `yaml:"responses"`
Method string `yaml:"-"`
Path string `yaml:"-"`
ResponseSchema jsonSchema `yaml:"-"`
RequestBodySchema jsonSchema `yaml:"-"`
}
type openAPIParameter struct {
Name string `yaml:"name"`
In string `yaml:"in"`
Description string `yaml:"description"`
Required bool `yaml:"required"`
Schema jsonSchema `yaml:"schema"`
}
type openAPIRequestBody struct {
Required bool `yaml:"required"`
Content map[string]openAPIMediaType `yaml:"content"`
}
type openAPIResponse struct {
Description string `yaml:"description"`
Content map[string]openAPIMediaType `yaml:"content"`
}
type openAPIMediaType struct {
Schema jsonSchema `yaml:"schema"`
}
func loadOpenAPISpec(path string) (*openAPISpec, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, errors.Wrap(err, "failed to read OpenAPI spec")
}
spec := &openAPISpec{}
if err := yaml.Unmarshal(data, spec); err != nil {
return nil, errors.Wrap(err, "failed to parse OpenAPI spec")
}
if spec.Paths == nil {
return nil, errors.New("OpenAPI spec has no paths")
}
return spec, nil
}
func buildOperationRegistry(spec *openAPISpec) (map[string]*openAPIOperation, error) {
registry := map[string]*openAPIOperation{}
for path, pathItem := range spec.Paths {
for method, operation := range pathItem {
if operation == nil || operation.OperationID == "" {
continue
}
if _, exists := registry[operation.OperationID]; exists {
return nil, errors.Errorf("duplicate OpenAPI operationId %q", operation.OperationID)
}
operation.Method = strings.ToUpper(method)
operation.Path = path
responseSchema, err := operationSuccessResponseSchema(spec, operation)
if err != nil {
return nil, errors.Wrapf(err, "failed to resolve response schema for %s", operation.OperationID)
}
operation.ResponseSchema = responseSchema
requestBodySchema, err := operationRequestBodySchema(spec, operation)
if err != nil {
return nil, errors.Wrapf(err, "failed to resolve request body schema for %s", operation.OperationID)
}
operation.RequestBodySchema = requestBodySchema
registry[operation.OperationID] = operation
}
}
return registry, nil
}
func operationSuccessResponseSchema(spec *openAPISpec, operation *openAPIOperation) (jsonSchema, error) {
response, ok := operation.Responses["200"]
if !ok || response.Content == nil {
return okSchema(), nil
}
mediaType, ok := response.Content["application/json"]
if !ok || mediaType.Schema == nil {
return okSchema(), nil
}
return resolveSchemaRef(spec, mediaType.Schema)
}
func operationRequestBodySchema(spec *openAPISpec, operation *openAPIOperation) (jsonSchema, error) {
if operation.RequestBody == nil {
return nil, nil
}
mediaType, ok := operation.RequestBody.Content["application/json"]
if !ok || mediaType.Schema == nil {
return jsonSchema{"type": "object"}, nil
}
return resolveSchemaRef(spec, mediaType.Schema)
}
func resolveSchemaRef(spec *openAPISpec, schema jsonSchema) (jsonSchema, error) {
defs := map[string]any{}
resolved, err := resolveSchemaValue(spec, schema, defs, map[string]bool{}, true)
if err != nil {
return nil, err
}
resolvedSchema, ok := resolved.(map[string]any)
if !ok {
return nil, errors.New("resolved schema is not an object")
}
if len(defs) > 0 {
resolvedSchema["$defs"] = defs
}
return jsonSchema(resolvedSchema), nil
}
func resolveSchemaValue(spec *openAPISpec, value any, defs map[string]any, resolving map[string]bool, inlineRef bool) (any, error) {
switch typed := value.(type) {
case jsonSchema:
return resolveSchemaMap(spec, map[string]any(typed), defs, resolving, inlineRef)
case map[string]any:
return resolveSchemaMap(spec, typed, defs, resolving, inlineRef)
case []any:
resolved := make([]any, 0, len(typed))
for _, item := range typed {
resolvedItem, err := resolveSchemaValue(spec, item, defs, resolving, false)
if err != nil {
return nil, err
}
resolved = append(resolved, resolvedItem)
}
return resolved, nil
default:
return value, nil
}
}
func resolveSchemaMap(spec *openAPISpec, schema map[string]any, defs map[string]any, resolving map[string]bool, inlineRef bool) (map[string]any, error) {
if ref, ok := schema["$ref"].(string); ok && ref != "" {
name, err := schemaComponentName(ref)
if err != nil {
return nil, err
}
if inlineRef {
return resolveComponentSchema(spec, name, defs, resolving)
}
if err := addSchemaDef(spec, name, defs, resolving); err != nil {
return nil, err
}
return map[string]any{"$ref": "#/$defs/" + name}, nil
}
resolved := make(map[string]any, len(schema))
for key, value := range schema {
resolvedValue, err := resolveSchemaValue(spec, value, defs, resolving, false)
if err != nil {
return nil, err
}
resolved[key] = resolvedValue
}
return resolved, nil
}
func resolveComponentSchema(spec *openAPISpec, name string, defs map[string]any, resolving map[string]bool) (map[string]any, error) {
component, ok := spec.Components.Schemas[name]
if !ok {
return nil, errors.Errorf("schema ref %q not found", schemaComponentRef(name))
}
resolving[name] = true
resolved, err := resolveSchemaMap(spec, map[string]any(component), defs, resolving, false)
delete(resolving, name)
if err != nil {
return nil, err
}
if _, ok := defs[name]; ok {
defs[name] = resolved
}
return resolved, nil
}
func addSchemaDef(spec *openAPISpec, name string, defs map[string]any, resolving map[string]bool) error {
if _, ok := defs[name]; ok {
return nil
}
component, ok := spec.Components.Schemas[name]
if !ok {
return errors.Errorf("schema ref %q not found", schemaComponentRef(name))
}
defs[name] = map[string]any{}
if resolving[name] {
return nil
}
resolving[name] = true
resolved, err := resolveSchemaMap(spec, map[string]any(component), defs, resolving, false)
delete(resolving, name)
if err != nil {
return err
}
defs[name] = resolved
return nil
}
func schemaComponentName(ref string) (string, error) {
const prefix = "#/components/schemas/"
if !strings.HasPrefix(ref, prefix) {
return "", errors.Errorf("unsupported schema ref %q", ref)
}
return strings.TrimPrefix(ref, prefix), nil
}
func schemaComponentRef(name string) string {
return "#/components/schemas/" + name
}
func okSchema() jsonSchema {
return jsonSchema{
"type": "object",
"properties": map[string]any{
"ok": map[string]any{"type": "boolean"},
},
}
}