From 013ba5ba6175a728d7b663f2bd8b05bf156dd649 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Tue, 31 Mar 2026 16:59:23 +0200 Subject: [PATCH] Replace kin-openapi with pb33f/libopenapi for OpenAPI parsing kin-openapi versions above v0.132.0 lack a valid license, making it a dead-end dependency. Replace it with pb33f/libopenapi (Apache 2.0), which provides full OpenAPI 3.0/3.1 support and is actively maintained. Key changes: - Rewrite pkg/tools/builtin/openapi.go to use libopenapi's high-level V3 data model (Document, PathItem, Operation, SchemaProxy, etc.) - Remove kin-openapi and its transitive dependencies from go.mod - Remove the "never bump kin-openapi" warning from the bump-go-dependencies skill since the dependency no longer exists Assisted-By: docker-agent --- .agents/skills/bump-go-dependencies/SKILL.md | 2 - go.mod | 16 ++- go.sum | 29 ++--- pkg/tools/builtin/openapi.go | 124 +++++++++++-------- pkg/tools/builtin/openapi_test.go | 57 +++++++++ 5 files changed, 146 insertions(+), 82 deletions(-) diff --git a/.agents/skills/bump-go-dependencies/SKILL.md b/.agents/skills/bump-go-dependencies/SKILL.md index 1e93fe01d..413495032 100644 --- a/.agents/skills/bump-go-dependencies/SKILL.md +++ b/.agents/skills/bump-go-dependencies/SKILL.md @@ -23,8 +23,6 @@ module/path current_version update_path new_version If the command produces no output, all direct dependencies are already up to date. Inform the user and stop. -NEVER bump `github.com/getkin/kin-openapi`. Version above v0.132.0 don't have a valid license. - ## 2. Update Each Dependency One by One For **each** outdated dependency, perform the following steps in order: diff --git a/go.mod b/go.mod index 8c1f46894..3090e6145 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,6 @@ require ( github.com/dop251/goja v0.0.0-20260311135729-065cd970411c github.com/fatih/color v1.19.0 github.com/fsnotify/fsnotify v1.9.0 - github.com/getkin/kin-openapi v0.132.0 github.com/go-git/go-git/v5 v5.17.1 github.com/goccy/go-yaml v1.19.2 github.com/golang-jwt/jwt/v5 v5.3.1 @@ -49,6 +48,7 @@ require ( github.com/modelcontextprotocol/go-sdk v1.4.1 github.com/natefinch/atomic v1.0.1 github.com/openai/openai-go/v3 v3.30.0 + github.com/pb33f/libopenapi v0.34.4 github.com/rivo/uniseg v0.4.7 github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82 github.com/spf13/cobra v1.10.2 @@ -73,6 +73,11 @@ require ( modernc.org/sqlite v1.48.0 ) +require ( + github.com/pb33f/jsonpath v0.8.2 // indirect + github.com/pb33f/ordered-map/v2 v2.3.1 // indirect +) + require ( cloud.google.com/go v0.123.0 // indirect cloud.google.com/go/auth v0.17.0 // indirect @@ -143,8 +148,6 @@ require ( github.com/go-git/go-billy/v5 v5.8.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/swag v0.23.0 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/snappy v0.0.4 // indirect @@ -158,7 +161,6 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.7 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.18.4 // indirect @@ -176,17 +178,13 @@ require ( github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect - github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect - github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect - github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect @@ -220,7 +218,7 @@ require ( go.opentelemetry.io/otel/metric v1.42.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect - go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.4 golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/text v0.35.0 // indirect diff --git a/go.sum b/go.sum index 4b2434c63..d5475bab4 100644 --- a/go.sum +++ b/go.sum @@ -216,8 +216,6 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= -github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= -github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -236,15 +234,9 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= -github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -297,7 +289,6 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= @@ -364,8 +355,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= @@ -377,10 +366,6 @@ github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0 github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= -github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= -github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= -github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/openai/openai-go/v3 v3.30.0 h1:T8VkhqAm6BuvxwpVG+Aw+H4TcYIsbj9nqytjpWcE/aU= @@ -391,8 +376,12 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= -github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pb33f/jsonpath v0.8.2 h1:Ou4C7zjYClBm97dfZjDCjdZGusJoynv/vrtiEKNfj2Y= +github.com/pb33f/jsonpath v0.8.2/go.mod h1:zBV5LJW4OQOPatmQE2QdKpGQJvhDTlE5IEj6ASaRNTo= +github.com/pb33f/libopenapi v0.34.4 h1:BWWXA3U4SlsHEvfczk+DJHu2O38ktgKw+zBEYaDZ2uI= +github.com/pb33f/libopenapi v0.34.4/go.mod h1:MsDdUlQ1CdrIDO5v26JfgBxQs7kcaOUEpMP3EqU6bI4= +github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= +github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -471,8 +460,6 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= -github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= @@ -531,8 +518,8 @@ go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pq go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go= -go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= +go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= diff --git a/pkg/tools/builtin/openapi.go b/pkg/tools/builtin/openapi.go index 784627973..ab21fa953 100644 --- a/pkg/tools/builtin/openapi.go +++ b/pkg/tools/builtin/openapi.go @@ -13,7 +13,10 @@ import ( "strings" "time" - "github.com/getkin/kin-openapi/openapi3" + "github.com/pb33f/libopenapi" + "github.com/pb33f/libopenapi/datamodel/high/base" + v3 "github.com/pb33f/libopenapi/datamodel/high/v3" + "go.yaml.in/yaml/v4" "github.com/docker/docker-agent/pkg/remote" "github.com/docker/docker-agent/pkg/tools" @@ -62,7 +65,7 @@ func (t *OpenAPITool) Tools(ctx context.Context) ([]tools.Tool, error) { } // fetchSpec retrieves and parses the OpenAPI specification from the configured URL. -func (t *OpenAPITool) fetchSpec(ctx context.Context) (*openapi3.T, error) { +func (t *OpenAPITool) fetchSpec(ctx context.Context) (*v3.Document, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, t.specURL, http.NoBody) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) @@ -92,35 +95,38 @@ func (t *OpenAPITool) fetchSpec(ctx context.Context) (*openapi3.T, error) { return nil, errors.New("OpenAPI spec exceeds 10MB size limit") } - loader := openapi3.NewLoader() - loader.IsExternalRefsAllowed = false - - spec, err := loader.LoadFromData(body) + doc, err := libopenapi.NewDocument(body) if err != nil { return nil, fmt.Errorf("failed to parse OpenAPI spec: %w", err) } - // Validate the spec but don't fail on validation errors. - // Some valid OpenAPI 3.1 features (e.g. "type": "null") are not yet - // supported by the validator in kin-openapi. - if err := spec.Validate(loader.Context); err != nil { - slog.Warn("OpenAPI spec validation reported issues; proceeding anyway", "url", t.specURL, "error", err) + model, buildErr := doc.BuildV3Model() + if buildErr != nil { + // Log validation issues but don't fail — some valid OpenAPI 3.1 + // features may not be fully supported by the validator. + slog.Warn("OpenAPI spec validation reported issues; proceeding anyway", "url", t.specURL, "error", buildErr) + } + + if model == nil { + return nil, errors.New("failed to build OpenAPI V3 model from spec") } - return spec, nil + return &model.Model, nil } // buildTools converts an OpenAPI spec into a list of tools. -func (t *OpenAPITool) buildTools(spec *openapi3.T) ([]tools.Tool, error) { +func (t *OpenAPITool) buildTools(spec *v3.Document) ([]tools.Tool, error) { baseURL, err := t.resolveBaseURL(spec) if err != nil { return nil, err } var result []tools.Tool - for path, pathItem := range spec.Paths.Map() { - for method, op := range pathOperations(pathItem) { - result = append(result, t.operationToTool(baseURL, path, method, op)) + if spec.Paths != nil && spec.Paths.PathItems != nil { + for path, pathItem := range spec.Paths.PathItems.FromOldest() { + for method, op := range pathOperations(pathItem) { + result = append(result, t.operationToTool(baseURL, path, method, op)) + } } } @@ -128,8 +134,8 @@ func (t *OpenAPITool) buildTools(spec *openapi3.T) ([]tools.Tool, error) { } // pathOperations returns all non-nil operations for a path item. -func pathOperations(item *openapi3.PathItem) map[string]*openapi3.Operation { - all := map[string]*openapi3.Operation{ +func pathOperations(item *v3.PathItem) map[string]*v3.Operation { + all := map[string]*v3.Operation{ http.MethodGet: item.Get, http.MethodPost: item.Post, http.MethodPut: item.Put, @@ -139,7 +145,7 @@ func pathOperations(item *openapi3.PathItem) map[string]*openapi3.Operation { http.MethodOptions: item.Options, } - ops := make(map[string]*openapi3.Operation, len(all)) + ops := make(map[string]*v3.Operation, len(all)) for m, op := range all { if op != nil { ops[m] = op @@ -150,7 +156,7 @@ func pathOperations(item *openapi3.PathItem) map[string]*openapi3.Operation { } // resolveBaseURL determines the base URL for API requests. -func (t *OpenAPITool) resolveBaseURL(spec *openapi3.T) (string, error) { +func (t *OpenAPITool) resolveBaseURL(spec *v3.Document) (string, error) { if len(spec.Servers) > 0 && spec.Servers[0].URL != "" { serverURL := spec.Servers[0].URL @@ -182,7 +188,7 @@ func (t *OpenAPITool) resolveBaseURL(spec *openapi3.T) (string, error) { } // operationToTool converts a single OpenAPI operation to a tool. -func (t *OpenAPITool) operationToTool(baseURL, path, method string, op *openapi3.Operation) tools.Tool { +func (t *OpenAPITool) operationToTool(baseURL, path, method string, op *v3.Operation) tools.Tool { name := operationToolName(path, method, op) desc := operationDescription(path, method, op) schema := operationSchema(op) @@ -208,16 +214,16 @@ func (t *OpenAPITool) operationToTool(baseURL, path, method string, op *openapi3 } // operationToolName returns a tool name derived from the operationId or the method+path. -func operationToolName(path, method string, op *openapi3.Operation) string { - if op.OperationID != "" { - return sanitizeToolName(op.OperationID) +func operationToolName(path, method string, op *v3.Operation) string { + if op.OperationId != "" { + return sanitizeToolName(op.OperationId) } return sanitizeToolName(strings.ToLower(method) + "_" + path) } // operationDescription returns a human-readable description for the operation. -func operationDescription(path, method string, op *openapi3.Operation) string { +func operationDescription(path, method string, op *v3.Operation) string { if op.Summary != "" { return op.Summary } @@ -233,32 +239,31 @@ func operationDescription(path, method string, op *openapi3.Operation) string { } // operationSchema builds a JSON Schema object describing the tool parameters. -func operationSchema(op *openapi3.Operation) map[string]any { +func operationSchema(op *v3.Operation) map[string]any { properties := map[string]any{} var required []string // Path and query parameters. - for _, ref := range op.Parameters { - if ref.Value == nil { + for _, p := range op.Parameters { + if p == nil { continue } - p := ref.Value - prop := schemaRefToProperty(p.Schema) + prop := schemaProxyToProperty(p.Schema) if p.Description != "" { prop["description"] = p.Description } properties[p.Name] = prop - if p.Required { + if p.Required != nil && *p.Required { required = append(required, p.Name) } } // JSON request body properties (prefixed with "body_"). if body := requestBodySchema(op); body != nil { - for name, propRef := range body.Properties { - properties["body_"+name] = schemaRefToProperty(propRef) + for name, propProxy := range body.Properties.FromOldest() { + properties["body_"+name] = schemaProxyToProperty(propProxy) } for _, req := range body.Required { required = append(required, "body_"+req) @@ -277,31 +282,35 @@ func operationSchema(op *openapi3.Operation) map[string]any { } // requestBodySchema extracts the JSON schema from an operation's request body, if any. -func requestBodySchema(op *openapi3.Operation) *openapi3.Schema { - if op.RequestBody == nil || op.RequestBody.Value == nil { +func requestBodySchema(op *v3.Operation) *base.Schema { + if op.RequestBody == nil || op.RequestBody.Content == nil { return nil } - jsonContent, ok := op.RequestBody.Value.Content["application/json"] - if !ok || jsonContent.Schema == nil || jsonContent.Schema.Value == nil { + jsonContent, ok := op.RequestBody.Content.Get("application/json") + if !ok || jsonContent.Schema == nil { return nil } - s := jsonContent.Schema.Value - if s.Properties == nil { + s := jsonContent.Schema.Schema() + if s == nil || s.Properties == nil { return nil } return s } -// schemaRefToProperty converts an OpenAPI schema reference to a JSON Schema property map. -func schemaRefToProperty(ref *openapi3.SchemaRef) map[string]any { - if ref == nil || ref.Value == nil { +// schemaProxyToProperty converts a libopenapi SchemaProxy to a JSON Schema property map. +func schemaProxyToProperty(proxy *base.SchemaProxy) map[string]any { + if proxy == nil { + return map[string]any{"type": "string"} + } + + s := proxy.Schema() + if s == nil { return map[string]any{"type": "string"} } - s := ref.Value prop := map[string]any{ "type": schemaType(s), } @@ -310,10 +319,16 @@ func schemaRefToProperty(ref *openapi3.SchemaRef) map[string]any { prop["description"] = s.Description } if len(s.Enum) > 0 { - prop["enum"] = s.Enum + enumValues := make([]any, 0, len(s.Enum)) + for _, node := range s.Enum { + if node != nil { + enumValues = append(enumValues, yamlNodeToValue(node)) + } + } + prop["enum"] = enumValues } if s.Default != nil { - prop["default"] = s.Default + prop["default"] = yamlNodeToValue(s.Default) } return prop @@ -321,16 +336,25 @@ func schemaRefToProperty(ref *openapi3.SchemaRef) map[string]any { // schemaType returns the JSON Schema type string for an OpenAPI schema. // Defaults to "string" when the type is unspecified. -func schemaType(s *openapi3.Schema) string { - if s.Type != nil { - if types := s.Type.Slice(); len(types) > 0 { - return types[0] - } +func schemaType(s *base.Schema) string { + if len(s.Type) > 0 { + return s.Type[0] } return "string" } +// yamlNodeToValue converts a yaml.Node to a native Go value, preserving the +// original type (int, float, bool, null) instead of returning everything as a +// string. +func yamlNodeToValue(node *yaml.Node) any { + var v any + if err := node.Decode(&v); err != nil { + return node.Value + } + return v +} + // sanitizeToolName converts a string into a valid tool name. func sanitizeToolName(name string) string { name = strings.NewReplacer( diff --git a/pkg/tools/builtin/openapi_test.go b/pkg/tools/builtin/openapi_test.go index fa5eecdce..772acd480 100644 --- a/pkg/tools/builtin/openapi_test.go +++ b/pkg/tools/builtin/openapi_test.go @@ -464,3 +464,60 @@ func TestSanitizeToolName(t *testing.T) { }) } } + +func TestOpenAPITool_EnumAndDefaultTypes(t *testing.T) { + t.Parallel() + + spec := `{ + "openapi": "3.0.0", + "info": { "title": "Test", "version": "1.0.0" }, + "paths": { + "/items": { + "get": { + "operationId": "listItems", + "summary": "List items", + "parameters": [ + { + "name": "status", + "in": "query", + "schema": { + "type": "string", + "enum": ["active", "inactive"], + "default": "active" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "enum": [10, 25, 50, 100], + "default": 25 + } + } + ], + "responses": { "200": {"description": "ok"} } + } + } + } + }` + + specServer := serveSpec(t, spec) + toolsList, err := NewOpenAPITool(specServer.URL+"/openapi.json", nil).Tools(t.Context()) + require.NoError(t, err) + require.Len(t, toolsList, 1) + + schema, ok := toolsList[0].Parameters.(map[string]any) + require.True(t, ok) + props := schema["properties"].(map[string]any) + + // String enum values should remain strings. + statusProp := props["status"].(map[string]any) + assert.Equal(t, []any{"active", "inactive"}, statusProp["enum"]) + assert.Equal(t, "active", statusProp["default"]) + + // Integer enum values should remain integers (not strings). + limitProp := props["limit"].(map[string]any) + assert.Equal(t, []any{10, 25, 50, 100}, limitProp["enum"]) + assert.Equal(t, 25, limitProp["default"]) +}