@lovrozagar/honey 0.6.8 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +514 -192
- package/bin/honey.js +25 -0
- package/dist/accepts.d.ts +7 -1
- package/dist/accepts.d.ts.map +1 -1
- package/dist/accepts.js +126 -0
- package/dist/accepts.js.map +1 -0
- package/dist/body-kind.d.ts +26 -0
- package/dist/body-kind.d.ts.map +1 -0
- package/dist/body-kind.js +70 -0
- package/dist/body-kind.js.map +1 -0
- package/dist/body-limit.d.ts +5 -1
- package/dist/body-limit.d.ts.map +1 -1
- package/dist/body-limit.js +126 -0
- package/dist/body-limit.js.map +1 -0
- package/dist/build/index.d.ts +19 -3
- package/dist/build/index.d.ts.map +1 -1
- package/dist/build/index.js +173 -0
- package/dist/build/index.js.map +1 -0
- package/dist/cli-args.d.ts +33 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +102 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli-go/auth.go +26 -0
- package/dist/cli-go/config.go +192 -0
- package/dist/cli-go/errors.go +75 -0
- package/dist/cli-go/multipart.go +79 -0
- package/dist/cli-go/output.go +163 -0
- package/dist/cli-go/root.go +30 -0
- package/dist/cli-go/stream.go +88 -0
- package/dist/cli-go/version.go +5 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +209 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/defaults.js +48 -0
- package/dist/client/defaults.js.map +1 -0
- package/dist/client/error.d.ts +35 -0
- package/dist/client/error.d.ts.map +1 -1
- package/dist/client/error.js +71 -0
- package/dist/client/error.js.map +1 -0
- package/dist/client/http.d.ts +67 -8
- package/dist/client/http.d.ts.map +1 -1
- package/dist/client/http.js +571 -0
- package/dist/client/http.js.map +1 -0
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +95 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/path.d.ts +26 -0
- package/dist/client/path.d.ts.map +1 -0
- package/dist/client/path.js +163 -0
- package/dist/client/path.js.map +1 -0
- package/dist/client/redirect-policy.d.ts +12 -0
- package/dist/client/redirect-policy.d.ts.map +1 -0
- package/dist/client/redirect-policy.js +20 -0
- package/dist/client/redirect-policy.js.map +1 -0
- package/dist/client/sdk.d.ts +36 -0
- package/dist/client/sdk.d.ts.map +1 -1
- package/dist/client/sdk.js +292 -0
- package/dist/client/sdk.js.map +1 -0
- package/dist/client/sse.d.ts +13 -0
- package/dist/client/sse.d.ts.map +1 -1
- package/dist/client/sse.js +129 -0
- package/dist/client/sse.js.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/client/types.js.map +1 -0
- package/dist/client/ws.d.ts +9 -2
- package/dist/client/ws.d.ts.map +1 -1
- package/dist/client/ws.js +112 -0
- package/dist/client/ws.js.map +1 -0
- package/dist/client-go/errors.go +236 -0
- package/dist/client-go/export.go +44 -0
- package/dist/client-go/invalidation.go +371 -0
- package/dist/client-go/nullable.go +62 -0
- package/dist/client-go/realtime.go +354 -0
- package/dist/client-go/result.go +20 -0
- package/dist/client-go/runtime.go +975 -0
- package/dist/client-go/sse.go +159 -0
- package/dist/client-go/transport.go +495 -0
- package/dist/client-go/ws.go +128 -0
- package/dist/client-info.d.ts +36 -0
- package/dist/client-info.d.ts.map +1 -0
- package/dist/client-info.js +53 -0
- package/dist/client-info.js.map +1 -0
- package/dist/client-mcp/README.md +10 -0
- package/dist/client-mcp/server.ts +221 -0
- package/dist/client-mcp/types.ts +19 -0
- package/dist/client-python/_errors.py +115 -0
- package/dist/client-python/_invalidation.py +494 -0
- package/dist/client-python/_realtime.py +179 -0
- package/dist/client-python/_runtime.py +860 -0
- package/dist/client-python/_sse.py +155 -0
- package/dist/client-python/_transport.py +366 -0
- package/dist/client-python/_ws.py +159 -0
- package/dist/client-python/py.typed +0 -0
- package/dist/client-rust/errors.rs +220 -0
- package/dist/client-rust/invalidation.rs +412 -0
- package/dist/client-rust/invalidation_sync.rs +93 -0
- package/dist/client-rust/nullable.rs +17 -0
- package/dist/client-rust/realtime.rs +702 -0
- package/dist/client-rust/result.rs +22 -0
- package/dist/client-rust/runtime.rs +980 -0
- package/dist/client-rust/runtime_sync.rs +484 -0
- package/dist/client-rust/sse.rs +188 -0
- package/dist/client-rust/ws.rs +167 -0
- package/dist/codegen-go-cli.d.ts.map +1 -1
- package/dist/codegen-go-cli.js +964 -0
- package/dist/codegen-go-cli.js.map +1 -0
- package/dist/codegen-go.d.ts +15 -3
- package/dist/codegen-go.d.ts.map +1 -1
- package/dist/codegen-go.js +872 -0
- package/dist/codegen-go.js.map +1 -0
- package/dist/codegen-hash.js +9 -0
- package/dist/codegen-hash.js.map +1 -0
- package/dist/codegen-ir.d.ts +69 -2
- package/dist/codegen-ir.d.ts.map +1 -1
- package/dist/codegen-ir.js +661 -0
- package/dist/codegen-ir.js.map +1 -0
- package/dist/codegen-lang.d.ts +65 -0
- package/dist/codegen-lang.d.ts.map +1 -0
- package/dist/codegen-lang.js +526 -0
- package/dist/codegen-lang.js.map +1 -0
- package/dist/codegen-load.d.ts +31 -0
- package/dist/codegen-load.d.ts.map +1 -0
- package/dist/codegen-load.js +138 -0
- package/dist/codegen-load.js.map +1 -0
- package/dist/codegen-loaders.js +55 -0
- package/dist/codegen-loaders.js.map +1 -0
- package/dist/codegen-mcp.d.ts.map +1 -1
- package/dist/codegen-mcp.js +368 -0
- package/dist/codegen-mcp.js.map +1 -0
- package/dist/codegen-python.d.ts +13 -1
- package/dist/codegen-python.d.ts.map +1 -1
- package/dist/codegen-python.js +828 -0
- package/dist/codegen-python.js.map +1 -0
- package/dist/codegen-route-tree-intern.d.ts +15 -5
- package/dist/codegen-route-tree-intern.d.ts.map +1 -1
- package/dist/codegen-route-tree-intern.js +221 -0
- package/dist/codegen-route-tree-intern.js.map +1 -0
- package/dist/codegen-rust.d.ts.map +1 -1
- package/dist/codegen-rust.js +1293 -0
- package/dist/codegen-rust.js.map +1 -0
- package/dist/codegen-sanitize.d.ts.map +1 -1
- package/dist/codegen-sanitize.js +56 -0
- package/dist/codegen-sanitize.js.map +1 -0
- package/dist/codegen-schema-naming.d.ts +5 -0
- package/dist/codegen-schema-naming.d.ts.map +1 -1
- package/dist/codegen-schema-naming.js +150 -0
- package/dist/codegen-schema-naming.js.map +1 -0
- package/dist/codegen-sdk-model.d.ts +71 -0
- package/dist/codegen-sdk-model.d.ts.map +1 -0
- package/dist/codegen-sdk-model.js +179 -0
- package/dist/codegen-sdk-model.js.map +1 -0
- package/dist/codegen.d.ts +41 -23
- package/dist/codegen.d.ts.map +1 -1
- package/dist/codegen.js +4610 -0
- package/dist/codegen.js.map +1 -0
- package/dist/context.d.ts +36 -6
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +242 -0
- package/dist/context.js.map +1 -0
- package/dist/cookie-sign.d.ts +16 -2
- package/dist/cookie-sign.d.ts.map +1 -1
- package/dist/cookie-sign.js +104 -0
- package/dist/cookie-sign.js.map +1 -0
- package/dist/cookie.d.ts +12 -0
- package/dist/cookie.d.ts.map +1 -1
- package/dist/cookie.js +138 -0
- package/dist/cookie.js.map +1 -0
- package/dist/cors.d.ts +1 -0
- package/dist/cors.d.ts.map +1 -1
- package/dist/cors.js +87 -0
- package/dist/cors.js.map +1 -0
- package/dist/crypto.d.ts +6 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +38 -0
- package/dist/crypto.js.map +1 -0
- package/dist/csrf.d.ts +19 -0
- package/dist/csrf.d.ts.map +1 -1
- package/dist/csrf.js +79 -0
- package/dist/csrf.js.map +1 -0
- package/dist/curl-logger.d.ts +14 -3
- package/dist/curl-logger.d.ts.map +1 -1
- package/dist/curl-logger.js +199 -0
- package/dist/curl-logger.js.map +1 -0
- package/dist/detect-runtime.js +13 -0
- package/dist/detect-runtime.js.map +1 -0
- package/dist/dict.d.ts +11 -0
- package/dist/dict.d.ts.map +1 -0
- package/dist/dict.js +13 -0
- package/dist/dict.js.map +1 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +44 -0
- package/dist/error.js.map +1 -0
- package/dist/errors.js +39 -0
- package/dist/errors.js.map +1 -0
- package/dist/etag.d.ts +2 -0
- package/dist/etag.d.ts.map +1 -1
- package/dist/etag.js +82 -0
- package/dist/etag.js.map +1 -0
- package/dist/feature-detect.d.ts +25 -0
- package/dist/feature-detect.d.ts.map +1 -0
- package/dist/feature-detect.js +144 -0
- package/dist/feature-detect.js.map +1 -0
- package/dist/feature-load.js +36 -0
- package/dist/feature-load.js.map +1 -0
- package/dist/feature-slots.d.ts +44 -0
- package/dist/feature-slots.d.ts.map +1 -0
- package/dist/feature-slots.js +48 -0
- package/dist/feature-slots.js.map +1 -0
- package/dist/fetch-request.d.ts +10 -0
- package/dist/fetch-request.d.ts.map +1 -0
- package/dist/fetch-request.js +13 -0
- package/dist/fetch-request.js.map +1 -0
- package/dist/gen-process.d.ts +15 -0
- package/dist/gen-process.d.ts.map +1 -0
- package/dist/gen-process.js +43 -0
- package/dist/gen-process.js.map +1 -0
- package/dist/gen-write.d.ts +19 -0
- package/dist/gen-write.d.ts.map +1 -0
- package/dist/gen-write.js +120 -0
- package/dist/gen-write.js.map +1 -0
- package/dist/glob.d.ts +21 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +115 -0
- package/dist/glob.js.map +1 -0
- package/dist/go-type-emitter.d.ts +18 -4
- package/dist/go-type-emitter.d.ts.map +1 -1
- package/dist/go-type-emitter.js +594 -0
- package/dist/go-type-emitter.js.map +1 -0
- package/dist/honey-response.d.ts.map +1 -1
- package/dist/honey-response.js +338 -0
- package/dist/honey-response.js.map +1 -0
- package/dist/i18n-slot.d.ts +8 -0
- package/dist/i18n-slot.d.ts.map +1 -1
- package/dist/i18n-slot.js +14 -0
- package/dist/i18n-slot.js.map +1 -0
- package/dist/i18n.d.ts +3 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +250 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +408 -148
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2324 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +3 -5
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +140 -0
- package/dist/init.js.map +1 -0
- package/dist/input.js +4 -0
- package/dist/input.js.map +1 -0
- package/dist/invalidate-check.d.ts +11 -0
- package/dist/invalidate-check.d.ts.map +1 -1
- package/dist/invalidate-check.js +163 -0
- package/dist/invalidate-check.js.map +1 -0
- package/dist/invoke-user.d.ts +35 -0
- package/dist/invoke-user.d.ts.map +1 -0
- package/dist/invoke-user.js +73 -0
- package/dist/invoke-user.js.map +1 -0
- package/dist/ip-restrict.d.ts +13 -1
- package/dist/ip-restrict.d.ts.map +1 -1
- package/dist/ip-restrict.js +60 -0
- package/dist/ip-restrict.js.map +1 -0
- package/dist/ip.d.ts +39 -0
- package/dist/ip.d.ts.map +1 -0
- package/dist/ip.js +209 -0
- package/dist/ip.js.map +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +113 -0
- package/dist/logger.js.map +1 -0
- package/dist/media-type.d.ts +30 -0
- package/dist/media-type.d.ts.map +1 -0
- package/dist/media-type.js +97 -0
- package/dist/media-type.js.map +1 -0
- package/dist/meta-spec-merge.d.ts +32 -0
- package/dist/meta-spec-merge.d.ts.map +1 -0
- package/dist/meta-spec-merge.js +68 -0
- package/dist/meta-spec-merge.js.map +1 -0
- package/dist/meta-spec.d.ts +16 -3
- package/dist/meta-spec.d.ts.map +1 -1
- package/dist/meta-spec.js +535 -0
- package/dist/meta-spec.js.map +1 -0
- package/dist/middleware.d.ts +23 -6
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +253 -0
- package/dist/middleware.js.map +1 -0
- package/dist/node-request.d.ts +57 -12
- package/dist/node-request.d.ts.map +1 -1
- package/dist/node-request.js +407 -0
- package/dist/node-request.js.map +1 -0
- package/dist/node.d.ts +20 -3
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +396 -0
- package/dist/node.js.map +1 -0
- package/dist/openapi/collect.d.ts +6 -3
- package/dist/openapi/collect.d.ts.map +1 -1
- package/dist/openapi/collect.js +30 -0
- package/dist/openapi/collect.js.map +1 -0
- package/dist/openapi/docs-page.d.ts +42 -0
- package/dist/openapi/docs-page.d.ts.map +1 -0
- package/dist/openapi/docs-page.js +80 -0
- package/dist/openapi/docs-page.js.map +1 -0
- package/dist/openapi/document.d.ts +4 -0
- package/dist/openapi/document.d.ts.map +1 -1
- package/dist/openapi/document.js +785 -0
- package/dist/openapi/document.js.map +1 -0
- package/dist/openapi/json-schema-slot.d.ts +7 -0
- package/dist/openapi/json-schema-slot.d.ts.map +1 -1
- package/dist/openapi/json-schema-slot.js +38 -0
- package/dist/openapi/json-schema-slot.js.map +1 -0
- package/dist/openapi/register.d.ts.map +1 -1
- package/dist/openapi/register.js +20 -0
- package/dist/openapi/register.js.map +1 -0
- package/dist/openapi/scalar.d.ts +3 -1
- package/dist/openapi/scalar.d.ts.map +1 -1
- package/dist/openapi/scalar.js +21 -0
- package/dist/openapi/scalar.js.map +1 -0
- package/dist/openapi/serve.d.ts +9 -0
- package/dist/openapi/serve.d.ts.map +1 -0
- package/dist/openapi/serve.js +36 -0
- package/dist/openapi/serve.js.map +1 -0
- package/dist/openapi/spec-factory.d.ts +30 -1
- package/dist/openapi/spec-factory.d.ts.map +1 -1
- package/dist/openapi/spec-factory.js +52 -0
- package/dist/openapi/spec-factory.js.map +1 -0
- package/dist/openapi/spec.d.ts +14 -5
- package/dist/openapi/spec.d.ts.map +1 -1
- package/dist/openapi/spec.js +50 -0
- package/dist/openapi/spec.js.map +1 -0
- package/dist/openapi/swagger.d.ts +3 -1
- package/dist/openapi/swagger.d.ts.map +1 -1
- package/dist/openapi/swagger.js +22 -0
- package/dist/openapi/swagger.js.map +1 -0
- package/dist/pattern.d.ts +71 -0
- package/dist/pattern.d.ts.map +1 -0
- package/dist/pattern.js +239 -0
- package/dist/pattern.js.map +1 -0
- package/dist/peer.d.ts +17 -0
- package/dist/peer.d.ts.map +1 -0
- package/dist/peer.js +50 -0
- package/dist/peer.js.map +1 -0
- package/dist/plugin.d.ts +67 -16
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +649 -0
- package/dist/plugin.js.map +1 -0
- package/dist/powered-by.d.ts.map +1 -1
- package/dist/powered-by.js +11 -0
- package/dist/powered-by.js.map +1 -0
- package/dist/pretty-json.d.ts.map +1 -1
- package/dist/pretty-json.js +47 -0
- package/dist/pretty-json.js.map +1 -0
- package/dist/producer-stream.d.ts +47 -0
- package/dist/producer-stream.d.ts.map +1 -0
- package/dist/producer-stream.js +267 -0
- package/dist/producer-stream.js.map +1 -0
- package/dist/proxy.d.ts +47 -8
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +274 -0
- package/dist/proxy.js.map +1 -0
- package/dist/python-type-emitter.d.ts +14 -1
- package/dist/python-type-emitter.d.ts.map +1 -1
- package/dist/python-type-emitter.js +157 -0
- package/dist/python-type-emitter.js.map +1 -0
- package/dist/realtime/bus.d.ts +17 -1
- package/dist/realtime/bus.d.ts.map +1 -1
- package/dist/realtime/bus.js +112 -0
- package/dist/realtime/bus.js.map +1 -0
- package/dist/realtime/register.d.ts +3 -0
- package/dist/realtime/register.d.ts.map +1 -0
- package/dist/realtime/register.js +10 -0
- package/dist/realtime/register.js.map +1 -0
- package/dist/realtime/route.d.ts +65 -26
- package/dist/realtime/route.d.ts.map +1 -1
- package/dist/realtime/route.js +72 -0
- package/dist/realtime/route.js.map +1 -0
- package/dist/realtime/server.d.ts +43 -0
- package/dist/realtime/server.d.ts.map +1 -0
- package/dist/realtime/server.js +255 -0
- package/dist/realtime/server.js.map +1 -0
- package/dist/request-id.d.ts +7 -0
- package/dist/request-id.d.ts.map +1 -1
- package/dist/request-id.js +16 -0
- package/dist/request-id.js.map +1 -0
- package/dist/request-limits.d.ts +3 -0
- package/dist/request-limits.d.ts.map +1 -0
- package/dist/request-limits.js +3 -0
- package/dist/request-limits.js.map +1 -0
- package/dist/request-path.d.ts +33 -0
- package/dist/request-path.d.ts.map +1 -0
- package/dist/request-path.js +178 -0
- package/dist/request-path.js.map +1 -0
- package/dist/request-to-curl.d.ts +7 -1
- package/dist/request-to-curl.d.ts.map +1 -1
- package/dist/request-to-curl.js +48 -0
- package/dist/request-to-curl.js.map +1 -0
- package/dist/response.d.ts +25 -2
- package/dist/response.d.ts.map +1 -1
- package/dist/response.js +313 -0
- package/dist/response.js.map +1 -0
- package/dist/rust-type-emitter.d.ts +14 -2
- package/dist/rust-type-emitter.d.ts.map +1 -1
- package/dist/rust-type-emitter.js +463 -0
- package/dist/rust-type-emitter.js.map +1 -0
- package/dist/secure-headers.d.ts.map +1 -1
- package/dist/secure-headers.js +53 -0
- package/dist/secure-headers.js.map +1 -0
- package/dist/serve-register.d.ts +4 -0
- package/dist/serve-register.d.ts.map +1 -1
- package/dist/serve-register.js +9 -0
- package/dist/serve-register.js.map +1 -0
- package/dist/serve-slot.d.ts +3 -0
- package/dist/serve-slot.d.ts.map +1 -1
- package/dist/serve-slot.js +52 -0
- package/dist/serve-slot.js.map +1 -0
- package/dist/serve.d.ts +18 -1
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +157 -0
- package/dist/serve.js.map +1 -0
- package/dist/server-timing.d.ts.map +1 -1
- package/dist/server-timing.js +51 -0
- package/dist/server-timing.js.map +1 -0
- package/dist/static.d.ts +9 -0
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +102 -0
- package/dist/static.js.map +1 -0
- package/dist/telemetry/otel.d.ts +2 -0
- package/dist/telemetry/otel.d.ts.map +1 -1
- package/dist/telemetry/otel.js +135 -0
- package/dist/telemetry/otel.js.map +1 -0
- package/dist/testing.d.ts +12 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +124 -0
- package/dist/testing.js.map +1 -0
- package/dist/timeout.d.ts +6 -0
- package/dist/timeout.d.ts.map +1 -1
- package/dist/timeout.js +36 -0
- package/dist/timeout.js.map +1 -0
- package/dist/tree.d.ts +133 -36
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +437 -0
- package/dist/tree.js.map +1 -0
- package/dist/trust.d.ts +21 -0
- package/dist/trust.d.ts.map +1 -0
- package/dist/trust.js +100 -0
- package/dist/trust.js.map +1 -0
- package/dist/ts-type-emitter.d.ts.map +1 -1
- package/dist/ts-type-emitter.js +150 -0
- package/dist/ts-type-emitter.js.map +1 -0
- package/dist/type-emitter.d.ts +16 -2
- package/dist/type-emitter.d.ts.map +1 -1
- package/dist/type-emitter.js +729 -0
- package/dist/type-emitter.js.map +1 -0
- package/dist/type-extractor.d.ts +8 -4
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +776 -0
- package/dist/type-extractor.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +96 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +14 -2
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +335 -0
- package/dist/validation.js.map +1 -0
- package/dist/with-headers.d.ts +15 -0
- package/dist/with-headers.d.ts.map +1 -0
- package/dist/with-headers.js +45 -0
- package/dist/with-headers.js.map +1 -0
- package/dist/ws/bun.d.ts +11 -5
- package/dist/ws/bun.d.ts.map +1 -1
- package/dist/ws/bun.js +82 -0
- package/dist/ws/bun.js.map +1 -0
- package/dist/ws/cloudflare.d.ts +25 -3
- package/dist/ws/cloudflare.d.ts.map +1 -1
- package/dist/ws/cloudflare.js +117 -0
- package/dist/ws/cloudflare.js.map +1 -0
- package/dist/ws/deno.d.ts +4 -4
- package/dist/ws/deno.d.ts.map +1 -1
- package/dist/ws/deno.js +133 -0
- package/dist/ws/deno.js.map +1 -0
- package/dist/ws/node.d.ts +20 -3
- package/dist/ws/node.d.ts.map +1 -1
- package/dist/ws/node.js +173 -0
- package/dist/ws/node.js.map +1 -0
- package/dist/ws/session.d.ts +11 -0
- package/dist/ws/session.d.ts.map +1 -0
- package/dist/ws/session.js +60 -0
- package/dist/ws/session.js.map +1 -0
- package/dist/ws/shared.d.ts +65 -0
- package/dist/ws/shared.d.ts.map +1 -0
- package/dist/ws/shared.js +103 -0
- package/dist/ws/shared.js.map +1 -0
- package/dist/ws-origin.d.ts +20 -0
- package/dist/ws-origin.d.ts.map +1 -0
- package/dist/ws-origin.js +41 -0
- package/dist/ws-origin.js.map +1 -0
- package/dist/yaml.d.ts +15 -1
- package/dist/yaml.d.ts.map +1 -1
- package/dist/yaml.js +101 -0
- package/dist/yaml.js.map +1 -0
- package/docs/invalidate-spec.md +13 -1
- package/docs/meta-spec.md +36 -12
- package/docs/packaging.md +43 -24
- package/docs/regression-matrix/README.md +32 -0
- package/docs/regression-matrix/review.md +44 -0
- package/docs/regression-matrix/ws1-3.md +97 -0
- package/docs/regression-matrix/ws4-8.md +107 -0
- package/docs/regression-matrix/ws9-12.md +266 -0
- package/docs/sdk.md +102 -52
- package/package.json +183 -52
- package/src/accepts.ts +91 -33
- package/src/body-kind.ts +67 -0
- package/src/body-limit.ts +102 -87
- package/src/build/index.ts +104 -54
- package/src/cli-args.ts +123 -0
- package/src/cli-go/auth.go +12 -1
- package/src/cli-go/config.go +29 -2
- package/src/cli-go/errors.go +28 -10
- package/src/cli-go/multipart.go +51 -27
- package/src/cli-go/output.go +58 -4
- package/src/cli-go/root.go +1 -1
- package/src/cli-go/version.go +2 -2
- package/src/cli.ts +154 -148
- package/src/client/error.ts +56 -1
- package/src/client/http.ts +479 -256
- package/src/client/index.ts +55 -17
- package/src/client/path.ts +169 -0
- package/src/client/redirect-policy.ts +19 -0
- package/src/client/sdk.ts +180 -101
- package/src/client/sse.ts +92 -66
- package/src/client/ws.ts +67 -31
- package/src/client-go/errors.go +41 -12
- package/src/client-go/export.go +44 -0
- package/src/client-go/invalidation.go +52 -54
- package/src/client-go/nullable.go +62 -0
- package/src/client-go/realtime.go +147 -110
- package/src/client-go/runtime.go +646 -88
- package/src/client-go/sse.go +104 -94
- package/src/client-go/transport.go +67 -27
- package/src/client-go/ws.go +49 -25
- package/src/client-info.ts +79 -0
- package/src/client-mcp/server.ts +133 -3
- package/src/client-python/_invalidation.py +207 -291
- package/src/client-python/_realtime.py +63 -40
- package/src/client-python/_runtime.py +501 -119
- package/src/client-python/_sse.py +130 -74
- package/src/client-python/_transport.py +37 -9
- package/src/client-python/_ws.py +50 -22
- package/src/client-rust/errors.rs +21 -1
- package/src/client-rust/invalidation.rs +202 -231
- package/src/client-rust/invalidation_sync.rs +16 -229
- package/src/client-rust/nullable.rs +17 -0
- package/src/client-rust/realtime.rs +217 -217
- package/src/client-rust/runtime.rs +602 -177
- package/src/client-rust/runtime_sync.rs +169 -145
- package/src/client-rust/sse.rs +142 -121
- package/src/client-rust/ws.rs +61 -37
- package/src/codegen-go-cli.ts +607 -728
- package/src/codegen-go.ts +710 -770
- package/src/codegen-ir.ts +381 -75
- package/src/codegen-lang.ts +517 -0
- package/src/codegen-load.ts +150 -0
- package/src/codegen-mcp.ts +207 -108
- package/src/codegen-python.ts +684 -816
- package/src/codegen-route-tree-intern.ts +51 -25
- package/src/codegen-rust.ts +946 -1240
- package/src/codegen-sanitize.ts +0 -4
- package/src/codegen-schema-naming.ts +19 -6
- package/src/codegen-sdk-model.ts +228 -0
- package/src/codegen.ts +1488 -1140
- package/src/context.ts +130 -11
- package/src/cookie-sign.ts +85 -16
- package/src/cookie.ts +66 -11
- package/src/cors.ts +55 -54
- package/src/crypto.ts +27 -5
- package/src/csrf.ts +57 -25
- package/src/curl-logger.ts +155 -114
- package/src/dict.ts +12 -0
- package/src/error.ts +4 -2
- package/src/etag.ts +58 -34
- package/src/feature-detect.ts +147 -0
- package/src/feature-slots.ts +70 -0
- package/src/fetch-request.ts +13 -0
- package/src/gen-process.ts +42 -0
- package/src/gen-write.ts +127 -0
- package/src/glob.ts +109 -0
- package/src/go-type-emitter.ts +406 -155
- package/src/honey-response.ts +36 -9
- package/src/i18n-slot.ts +10 -0
- package/src/i18n.ts +98 -23
- package/src/index.ts +1887 -1756
- package/src/init.ts +40 -28
- package/src/invalidate-check.ts +42 -4
- package/src/invoke-user.ts +95 -0
- package/src/ip-restrict.ts +47 -161
- package/src/ip.ts +194 -0
- package/src/logger.ts +65 -36
- package/src/media-type.ts +104 -0
- package/src/meta-spec-merge.ts +80 -0
- package/src/meta-spec.ts +0 -0
- package/src/middleware.ts +112 -12
- package/src/node-request.ts +242 -52
- package/src/node.ts +318 -75
- package/src/openapi/collect.ts +19 -44
- package/src/openapi/docs-page.ts +89 -0
- package/src/openapi/document.ts +503 -224
- package/src/openapi/json-schema-slot.ts +40 -0
- package/src/openapi/register.ts +7 -2
- package/src/openapi/scalar.ts +11 -10
- package/src/openapi/serve.ts +39 -0
- package/src/openapi/spec-factory.ts +50 -1
- package/src/openapi/spec.ts +53 -30
- package/src/openapi/swagger.ts +12 -11
- package/src/pattern.ts +245 -0
- package/src/peer.ts +53 -0
- package/src/plugin.ts +270 -185
- package/src/powered-by.ts +2 -2
- package/src/pretty-json.ts +25 -9
- package/src/producer-stream.ts +296 -0
- package/src/proxy.ts +253 -55
- package/src/python-type-emitter.ts +68 -56
- package/src/realtime/bus.ts +56 -9
- package/src/realtime/register.ts +13 -0
- package/src/realtime/route.ts +129 -80
- package/src/realtime/server.ts +298 -0
- package/src/request-id.ts +13 -3
- package/src/request-limits.ts +2 -0
- package/src/request-path.ts +165 -0
- package/src/request-to-curl.ts +28 -7
- package/src/response.ts +143 -103
- package/src/rust-type-emitter.ts +265 -170
- package/src/secure-headers.ts +8 -4
- package/src/serve-register.ts +4 -0
- package/src/serve-slot.ts +38 -0
- package/src/serve.ts +113 -25
- package/src/server-timing.ts +11 -7
- package/src/static.ts +85 -32
- package/src/telemetry/otel.ts +29 -7
- package/src/testing.ts +76 -14
- package/src/timeout.ts +15 -7
- package/src/tree.ts +405 -333
- package/src/trust.ts +117 -0
- package/src/ts-type-emitter.ts +18 -12
- package/src/type-emitter.ts +227 -89
- package/src/type-extractor.ts +562 -424
- package/src/types.ts +8 -0
- package/src/validation.ts +221 -96
- package/src/with-headers.ts +45 -0
- package/src/ws/bun.ts +53 -15
- package/src/ws/cloudflare.ts +81 -19
- package/src/ws/deno.ts +124 -36
- package/src/ws/node.ts +173 -79
- package/src/ws/session.ts +73 -0
- package/src/ws/shared.ts +140 -0
- package/src/ws-origin.ts +53 -0
- package/src/yaml.ts +45 -22
- package/dist/client-mcp/server.d.ts +0 -10
- package/dist/client-mcp/server.d.ts.map +0 -1
- package/dist/client-mcp/types.d.ts +0 -16
- package/dist/client-mcp/types.d.ts.map +0 -1
- package/dist/cloudflare/realtime.d.ts +0 -30
- package/dist/cloudflare/realtime.d.ts.map +0 -1
- package/dist/realtime/buffer.d.ts +0 -18
- package/dist/realtime/buffer.d.ts.map +0 -1
- package/dist/realtime/longpoll.d.ts +0 -23
- package/dist/realtime/longpoll.d.ts.map +0 -1
- package/dist/realtime/protocol.d.ts +0 -50
- package/dist/realtime/protocol.d.ts.map +0 -1
- package/dist/realtime/runtime.d.ts +0 -74
- package/dist/realtime/runtime.d.ts.map +0 -1
- package/src/cloudflare/realtime.ts +0 -234
- package/src/realtime/buffer.ts +0 -101
- package/src/realtime/longpoll.ts +0 -148
- package/src/realtime/protocol.ts +0 -128
- package/src/realtime/runtime.ts +0 -427
package/src/codegen-rust.ts
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
|
-
/* Rust SDK emitter — generates a complete Rust crate from an OpenAPI 3.1 spec.
|
|
1
|
+
/* Rust SDK emitter — generates a complete Rust crate from an OpenAPI 3.1 spec.
|
|
2
|
+
*
|
|
3
|
+
* Input is the SDK request model (codegen-sdk-model.ts). Every crate-level type name comes from one
|
|
4
|
+
* NameScope seeded with the prelude and the runtime's own types; fields, params and methods are
|
|
5
|
+
* claimed per scope; spec strings reach source only through rustString / rustDoc.
|
|
6
|
+
*/
|
|
2
7
|
|
|
3
8
|
import { readFileSync } from "node:fs"
|
|
4
9
|
import { fileURLToPath } from "node:url"
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { methodsOf, namespacesOf
|
|
8
|
-
import type { IRNamespace,
|
|
10
|
+
import type { OpenApiSpecInput } from "./codegen.ts"
|
|
11
|
+
import { serviceMapOf } from "./codegen.ts"
|
|
12
|
+
import { irErrorEnvelope, methodsOf, namespacesOf } from "./codegen-ir.ts"
|
|
13
|
+
import type { IRNamespace, IRSchema } from "./codegen-ir.ts"
|
|
9
14
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
NameScope,
|
|
16
|
+
RUST_PRELUDE_TYPES,
|
|
17
|
+
cmpCodeUnit,
|
|
18
|
+
rustDoc,
|
|
19
|
+
rustPlainSnake,
|
|
20
|
+
rustString,
|
|
21
|
+
rustTypeIdent,
|
|
22
|
+
rustValueIdent,
|
|
23
|
+
} from "./codegen-lang.ts"
|
|
24
|
+
import { buildSdkModel, isJsonMedia, valueCycleSchemas } from "./codegen-sdk-model.ts"
|
|
25
|
+
import type { SdkModel, SdkOp } from "./codegen-sdk-model.ts"
|
|
26
|
+
import { irRenderTopLevelRust, irRenderUseRust } from "./rust-type-emitter.ts"
|
|
27
|
+
import type { RustTypeNames } from "./rust-type-emitter.ts"
|
|
18
28
|
|
|
19
29
|
export type RustSDKOptions = {
|
|
20
30
|
crateName?: string
|
|
@@ -31,15 +41,6 @@ export type GeneratedRustSDK = {
|
|
|
31
41
|
serviceMap: Record<string, Record<string, unknown>>
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
type OpenApiSpec = {
|
|
35
|
-
components?: {
|
|
36
|
-
schemas?: Record<string, Record<string, unknown>>
|
|
37
|
-
}
|
|
38
|
-
info: { description?: string; title: string; version: string }
|
|
39
|
-
openapi: string
|
|
40
|
-
paths: Record<string, Record<string, Record<string, unknown>>>
|
|
41
|
-
}
|
|
42
|
-
|
|
43
44
|
let runtimeCache: Map<string, string> | null = null
|
|
44
45
|
|
|
45
46
|
export function loadRustRuntimeTemplates(): Map<string, string> {
|
|
@@ -55,6 +56,7 @@ export function loadRustRuntimeTemplates(): Map<string, string> {
|
|
|
55
56
|
"ws.rs",
|
|
56
57
|
"realtime.rs",
|
|
57
58
|
"runtime_sync.rs",
|
|
59
|
+
"nullable.rs",
|
|
58
60
|
]
|
|
59
61
|
const cache = new Map<string, string>()
|
|
60
62
|
for (const name of names) {
|
|
@@ -79,351 +81,395 @@ export function validateCrateName(name: string): string {
|
|
|
79
81
|
return name
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
84
|
+
/* Names the runtime modules and generated client declare or glob-import. */
|
|
85
|
+
const RUST_RUNTIME_TYPES = [
|
|
86
|
+
"ApiError",
|
|
87
|
+
"AuthState",
|
|
88
|
+
"BadGatewayError",
|
|
89
|
+
"BadRequestError",
|
|
90
|
+
"Client",
|
|
91
|
+
"ClientConfig",
|
|
92
|
+
"ClientInner",
|
|
93
|
+
"ConflictError",
|
|
94
|
+
"ConnectionState",
|
|
95
|
+
"ErrField",
|
|
96
|
+
"Error",
|
|
97
|
+
"ForbiddenError",
|
|
98
|
+
"GatewayTimeoutError",
|
|
99
|
+
"InternalServerError",
|
|
100
|
+
"InvalidationConfig",
|
|
101
|
+
"LogEntry",
|
|
102
|
+
"LongpollConn",
|
|
103
|
+
"LongpollState",
|
|
104
|
+
"LongpollTransport",
|
|
105
|
+
"NotFoundError",
|
|
106
|
+
"OnAuthExpiredHook",
|
|
107
|
+
"OnLogHook",
|
|
108
|
+
"OnRequestHook",
|
|
109
|
+
"OnResponseHook",
|
|
110
|
+
"QueryValue",
|
|
111
|
+
"RateLimitError",
|
|
112
|
+
"RealtimeError",
|
|
113
|
+
"RequestBody",
|
|
114
|
+
"RequestContext",
|
|
115
|
+
"RequestMeta",
|
|
116
|
+
"RequestResult",
|
|
117
|
+
"ResponseContext",
|
|
118
|
+
"ResponseMeta",
|
|
119
|
+
"ResumableConnection",
|
|
120
|
+
"ResumableConnectionOpts",
|
|
121
|
+
"SdkResult",
|
|
122
|
+
"ServiceEntry",
|
|
123
|
+
"ServiceUnavailableError",
|
|
124
|
+
"SseConn",
|
|
125
|
+
"SseEvent",
|
|
126
|
+
"SseParser",
|
|
127
|
+
"SseState",
|
|
128
|
+
"SseTransport",
|
|
129
|
+
"StaleEntry",
|
|
130
|
+
"StaleTracker",
|
|
131
|
+
"StaleTrackerSync",
|
|
132
|
+
"StatusError",
|
|
133
|
+
"SyncAuthState",
|
|
134
|
+
"SyncClient",
|
|
135
|
+
"SyncClientConfig",
|
|
136
|
+
"SyncClientInner",
|
|
137
|
+
"SyncOnRequestHook",
|
|
138
|
+
"SyncOnResponseHook",
|
|
139
|
+
"SyncRequestBody",
|
|
140
|
+
"SyncRequestContext",
|
|
141
|
+
"SyncRequestResult",
|
|
142
|
+
"SyncResponseContext",
|
|
143
|
+
"Transport",
|
|
144
|
+
"TransportConn",
|
|
145
|
+
"TransportKind",
|
|
146
|
+
"TransportOpts",
|
|
147
|
+
"TypedWebSocket",
|
|
148
|
+
"UnauthorizedError",
|
|
149
|
+
"UnprocessableEntityError",
|
|
150
|
+
"WsConn",
|
|
151
|
+
"WsTransport",
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
type RustNames = {
|
|
155
|
+
scope: NameScope
|
|
156
|
+
types: RustTypeNames
|
|
157
|
+
decls: Map<string, string>
|
|
121
158
|
}
|
|
122
159
|
|
|
123
|
-
function
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
/* octet-stream takes precedence: a single binary body has no sibling JSON/multipart
|
|
130
|
-
* entry in practice; detect early so the emitted method accepts a stream/reader. */
|
|
131
|
-
if (content["application/octet-stream"]) {
|
|
132
|
-
return { kind: "stream", type: "" }
|
|
133
|
-
}
|
|
134
|
-
if (content["multipart/form-data"]) {
|
|
135
|
-
return { kind: "multipart", type: "reqwest::multipart::Form" }
|
|
160
|
+
function buildRustNames(model: SdkModel): RustNames {
|
|
161
|
+
const scope = new NameScope([...RUST_PRELUDE_TYPES, ...RUST_RUNTIME_TYPES])
|
|
162
|
+
const schemaNames = new Map<string, string>()
|
|
163
|
+
for (const name of model.schemaNames) {
|
|
164
|
+
const base = rustTypeIdent(name)
|
|
165
|
+
schemaNames.set(name, scope.has(base) ? scope.claim(`${base}Model`) : scope.claim(base))
|
|
136
166
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
167
|
+
const hoistKeys = new Map<string, string>()
|
|
168
|
+
return {
|
|
169
|
+
decls: new Map(),
|
|
170
|
+
scope,
|
|
171
|
+
types: {
|
|
172
|
+
cyclic: valueCycleSchemas(model.ir.schemas),
|
|
173
|
+
hoist: (key, base) => {
|
|
174
|
+
const hit = hoistKeys.get(key)
|
|
175
|
+
if (hit) return hit
|
|
176
|
+
const name = scope.claim(rustTypeIdent(base))
|
|
177
|
+
hoistKeys.set(key, name)
|
|
178
|
+
return name
|
|
179
|
+
},
|
|
180
|
+
ref: (name) => schemaNames.get(name) ?? rustTypeIdent(name),
|
|
181
|
+
resolve: model.resolve,
|
|
182
|
+
},
|
|
145
183
|
}
|
|
146
|
-
return { kind: "json", type: "serde_json::Value" }
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
type QueryParam = {
|
|
150
|
-
name: string
|
|
151
|
-
required: boolean
|
|
152
|
-
schema: Record<string, unknown>
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function getQueryParams(op: Record<string, unknown>): QueryParam[] {
|
|
156
|
-
const parameters = op.parameters as Array<Record<string, unknown>> | undefined
|
|
157
|
-
if (!parameters) return []
|
|
158
|
-
return parameters
|
|
159
|
-
.filter((p) => p.in === "query")
|
|
160
|
-
.map((p) => ({
|
|
161
|
-
name: String(p.name),
|
|
162
|
-
required: p.required === true,
|
|
163
|
-
schema: (p.schema as Record<string, unknown>) ?? { type: "string" },
|
|
164
|
-
}))
|
|
165
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
166
184
|
}
|
|
167
185
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
186
|
+
/* ── per-op plan ── */
|
|
187
|
+
|
|
188
|
+
type RustOptField = { ident: string; type: string; wire: string; where: "query" | "header"; required: boolean }
|
|
189
|
+
|
|
190
|
+
type ErrEnvelopeInfo = { keyEnumName: string; keys: string[]; status: number; structName: string; variantName: string }
|
|
191
|
+
|
|
192
|
+
type RustOpPlan = {
|
|
193
|
+
op: SdkOp
|
|
194
|
+
fnName: string
|
|
195
|
+
optsName: string
|
|
196
|
+
optFields: RustOptField[]
|
|
197
|
+
pathParams: Array<{ local: string; wire: string }>
|
|
198
|
+
bodyParam?: {
|
|
199
|
+
local: string
|
|
200
|
+
asyncType: string
|
|
201
|
+
syncType: string
|
|
202
|
+
generic?: { async: string; asyncWhere?: string; sync: string }
|
|
203
|
+
}
|
|
204
|
+
/** Rust type of the decoded success payload, "" for none */
|
|
205
|
+
resultType: string
|
|
206
|
+
errorEnum?: string
|
|
207
|
+
envelopes: ErrEnvelopeInfo[]
|
|
208
|
+
/** invalidation map keys */
|
|
209
|
+
resource: string
|
|
210
|
+
action: string
|
|
173
211
|
}
|
|
174
212
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
213
|
+
const OPTS_RESERVED = [
|
|
214
|
+
"last_event_id",
|
|
215
|
+
"reconnect_token",
|
|
216
|
+
"protocols",
|
|
217
|
+
"idempotency_key",
|
|
218
|
+
"headers",
|
|
219
|
+
"timeout",
|
|
220
|
+
"cancel_token",
|
|
221
|
+
"sync_cancel_token",
|
|
222
|
+
"max_reconnect_attempts",
|
|
223
|
+
"reconnect_delay_ms",
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
/** Locals every emitted method body uses. */
|
|
227
|
+
const METHOD_LOCALS = [
|
|
228
|
+
"self",
|
|
229
|
+
"opts",
|
|
230
|
+
"query",
|
|
231
|
+
"url_path",
|
|
232
|
+
"result",
|
|
233
|
+
"out",
|
|
234
|
+
"req",
|
|
235
|
+
"resp",
|
|
236
|
+
"stream",
|
|
237
|
+
"cfg",
|
|
238
|
+
"client",
|
|
239
|
+
"auth",
|
|
240
|
+
"body_value",
|
|
241
|
+
"call_headers",
|
|
242
|
+
"path_params",
|
|
243
|
+
"concrete_path",
|
|
244
|
+
"selector",
|
|
245
|
+
"request_meta",
|
|
246
|
+
"stale",
|
|
247
|
+
"targets",
|
|
248
|
+
"headers",
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
/** Per-status error envelopes (standard shapes only), read from the IR with refs resolved. */
|
|
252
|
+
function errorEnvelopes(op: SdkOp, opPascal: string, model: SdkModel, names: RustNames): ErrEnvelopeInfo[] {
|
|
253
|
+
const out: ErrEnvelopeInfo[] = []
|
|
254
|
+
for (const [status, response] of Object.entries(op.ir.responses).sort(([a], [b]) => cmpCodeUnit(a, b))) {
|
|
181
255
|
const code = Number.parseInt(status, 10)
|
|
182
|
-
if (code < 400) continue
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
if (!schema) continue
|
|
186
|
-
const envelope = isStandardErrEnvelope(schema)
|
|
256
|
+
if (!/^[0-9]{3}$/.test(status) || code < 400) continue
|
|
257
|
+
if (!response.schema || !response.contentType || !isJsonMedia(response.contentType)) continue
|
|
258
|
+
const envelope = irErrorEnvelope(response.schema, model.resolve)
|
|
187
259
|
if (!envelope) continue
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
keyEnumName: `${opPascal}Err${code}Key`,
|
|
260
|
+
out.push({
|
|
261
|
+
keyEnumName: names.scope.claim(`${opPascal}Err${code}Key`),
|
|
191
262
|
keys: envelope.keys,
|
|
192
263
|
status: code,
|
|
264
|
+
structName: names.scope.claim(`${opPascal}Err${code}`),
|
|
193
265
|
variantName: `Status${code}`,
|
|
194
266
|
})
|
|
195
267
|
}
|
|
196
|
-
return
|
|
268
|
+
return out
|
|
197
269
|
}
|
|
198
270
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const envelopes = getOperationErrorEnvelopes(opId, op)
|
|
202
|
-
if (envelopes.length === 0) return []
|
|
203
|
-
const opPascal = pascalizeOpId(opId)
|
|
204
|
-
const errorEnumName = `${opPascal}Error`
|
|
271
|
+
function emitErrorTypes(plan: RustOpPlan): string[] {
|
|
272
|
+
if (plan.envelopes.length === 0 || !plan.errorEnum) return []
|
|
205
273
|
const l: string[] = []
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
/* key enum */
|
|
209
|
-
const variants = env.keys.map((k) => rustPascal(k))
|
|
274
|
+
for (const env of plan.envelopes) {
|
|
275
|
+
const variants = new NameScope()
|
|
210
276
|
l.push(`#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]`)
|
|
211
|
-
l.push(`#[serde(rename_all = "snake_case")]`)
|
|
212
277
|
l.push(`pub enum ${env.keyEnumName} {`)
|
|
213
|
-
for (const
|
|
278
|
+
for (const k of env.keys) {
|
|
279
|
+
const v = variants.claim(k === "" ? "Empty" : rustTypeIdent(k))
|
|
280
|
+
l.push(`\t#[serde(rename = ${rustString(k)})]`)
|
|
214
281
|
l.push(`\t${v},`)
|
|
215
282
|
}
|
|
216
283
|
l.push(`}`)
|
|
217
284
|
l.push(``)
|
|
218
|
-
|
|
219
|
-
/* per-status struct with error_key typed field */
|
|
220
|
-
const structName = `${opPascal}Err${env.status}`
|
|
221
285
|
l.push(`#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]`)
|
|
222
|
-
l.push(`pub struct ${structName} {`)
|
|
286
|
+
l.push(`pub struct ${env.structName} {`)
|
|
223
287
|
l.push(`\tpub error_key: ${env.keyEnumName},`)
|
|
224
288
|
l.push(`\tpub message: String,`)
|
|
225
289
|
l.push(`\tpub fields: std::collections::HashMap<String, Vec<crate::types::ErrField>>,`)
|
|
226
290
|
l.push(`}`)
|
|
227
291
|
l.push(``)
|
|
228
292
|
}
|
|
229
|
-
|
|
230
293
|
l.push(`#[derive(Debug)]`)
|
|
231
|
-
l.push(`pub enum ${
|
|
232
|
-
for (const env of envelopes) {
|
|
233
|
-
const structName = `${opPascal}Err${env.status}`
|
|
234
|
-
l.push(`\t${env.variantName}(${structName}),`)
|
|
235
|
-
}
|
|
294
|
+
l.push(`pub enum ${plan.errorEnum} {`)
|
|
295
|
+
for (const env of plan.envelopes) l.push(`\t${env.variantName}(${env.structName}),`)
|
|
236
296
|
l.push(`\tOther(Error),`)
|
|
237
297
|
l.push(`}`)
|
|
238
298
|
l.push(``)
|
|
239
299
|
return l
|
|
240
300
|
}
|
|
241
301
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
op
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const segments = irOp.id.split(".")
|
|
292
|
-
const isTopLevel = segments.length === 1
|
|
293
|
-
const resource = segments[0] ?? irOp.id
|
|
294
|
-
const action = isTopLevel ? "_call" : segments.slice(1).join(".")
|
|
295
|
-
const namePath = segments.slice(0, -1)
|
|
296
|
-
const methodAction = segments[segments.length - 1] ?? irOp.id
|
|
297
|
-
return {
|
|
298
|
-
action,
|
|
299
|
-
bodyInfo: rustBodyInfo(rawOp),
|
|
300
|
-
description: String(rawOp.description ?? ""),
|
|
301
|
-
httpMethod: irOp.method,
|
|
302
|
-
invalidates: invalidatesOf(irOp, rawOp),
|
|
303
|
-
isIdempotent: rawOp["x-idempotency-key"] === true,
|
|
304
|
-
isRealtime: rawOp["x-realtime"] === true,
|
|
305
|
-
isSSE: isSSEOperation(rawOp),
|
|
306
|
-
isWS: rawOp["x-websocket"] === true,
|
|
307
|
-
methodAction,
|
|
308
|
-
namePath,
|
|
309
|
-
op: rawOp,
|
|
310
|
-
opId: irOp.id,
|
|
311
|
-
path: irOp.path,
|
|
312
|
-
pathParams: irOp.params.path.map((p) => p.name),
|
|
313
|
-
queryParams: getQueryParams(rawOp),
|
|
314
|
-
resource,
|
|
315
|
-
responseType: rustResponseType(rawOp),
|
|
316
|
-
summary: String(rawOp.summary ?? ""),
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function collectMethodsFromIR(ns: IRNamespace, spec: OpenApiSpec): MethodInfo[] {
|
|
321
|
-
const rawOpById = new Map<string, Record<string, unknown>>()
|
|
322
|
-
for (const [, pathMethods] of Object.entries(spec.paths ?? {})) {
|
|
323
|
-
for (const [, operation] of Object.entries(pathMethods)) {
|
|
324
|
-
const op = operation as Record<string, unknown>
|
|
325
|
-
const id = op.operationId as string | undefined
|
|
326
|
-
if (id) rawOpById.set(id, op)
|
|
302
|
+
function planOp(
|
|
303
|
+
op: SdkOp,
|
|
304
|
+
fnName: string,
|
|
305
|
+
pathSegs: string[],
|
|
306
|
+
names: RustNames,
|
|
307
|
+
throwOnError: boolean,
|
|
308
|
+
model: SdkModel,
|
|
309
|
+
): RustOpPlan {
|
|
310
|
+
const opPascal = pathSegs.map(rustTypeIdent).join("")
|
|
311
|
+
const optsName = names.scope.claim(`${opPascal}Opts`)
|
|
312
|
+
const locals = new NameScope(METHOD_LOCALS)
|
|
313
|
+
const plan: RustOpPlan = {
|
|
314
|
+
action: op.segments.length === 1 ? "_call" : op.segments.slice(1).join("."),
|
|
315
|
+
envelopes: [],
|
|
316
|
+
fnName,
|
|
317
|
+
op,
|
|
318
|
+
optFields: [],
|
|
319
|
+
optsName,
|
|
320
|
+
pathParams: [],
|
|
321
|
+
resource: op.segments[0] ?? op.id,
|
|
322
|
+
resultType: "",
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const hasBody = op.body !== undefined && op.stream !== "ws" && op.stream !== "realtime"
|
|
326
|
+
const bodyLocal = hasBody ? locals.claim("body") : ""
|
|
327
|
+
for (const p of op.pathParams) plan.pathParams.push({ local: locals.claim(rustPlainSnake(p.name)), wire: p.name })
|
|
328
|
+
|
|
329
|
+
if (op.stream !== "realtime") {
|
|
330
|
+
const fieldScope = new NameScope(OPTS_RESERVED)
|
|
331
|
+
const params: Array<[{ name: string; schema: IRSchema; required?: boolean }, "query" | "header"]> = [
|
|
332
|
+
...op.query.map((p): [typeof p, "query"] => [p, "query"]),
|
|
333
|
+
...op.headers.map((p): [typeof p, "header"] => [p, "header"]),
|
|
334
|
+
]
|
|
335
|
+
for (const [p, where] of params) {
|
|
336
|
+
if (op.stream === "sse" && where === "query" && /^last[-_]?event[-_]?id$/i.test(p.name)) continue
|
|
337
|
+
const ident = fieldScope.claim(rustPlainSnake(p.name))
|
|
338
|
+
const t = irRenderUseRust(p.schema, {
|
|
339
|
+
decls: names.decls,
|
|
340
|
+
fieldName: p.name,
|
|
341
|
+
names: names.types,
|
|
342
|
+
parentName: optsName,
|
|
343
|
+
})
|
|
344
|
+
plan.optFields.push({
|
|
345
|
+
ident,
|
|
346
|
+
required: p.required === true,
|
|
347
|
+
type: p.required === true || t.startsWith("Option<") ? t : `Option<${t}>`,
|
|
348
|
+
where,
|
|
349
|
+
wire: p.name,
|
|
350
|
+
})
|
|
327
351
|
}
|
|
328
352
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
353
|
+
|
|
354
|
+
if (hasBody && op.body) {
|
|
355
|
+
const b = op.body
|
|
356
|
+
switch (b.kind) {
|
|
357
|
+
case "json": {
|
|
358
|
+
let t: string
|
|
359
|
+
if (b.schema.kind === "ref") t = names.types.ref(b.schema.name)
|
|
360
|
+
else
|
|
361
|
+
t = irRenderUseRust(b.schema, {
|
|
362
|
+
decls: names.decls,
|
|
363
|
+
fieldName: "Body",
|
|
364
|
+
names: names.types,
|
|
365
|
+
parentName: opPascal,
|
|
366
|
+
})
|
|
367
|
+
plan.bodyParam = { asyncType: `&${t}`, local: bodyLocal, syncType: `&${t}` }
|
|
368
|
+
break
|
|
369
|
+
}
|
|
370
|
+
case "form":
|
|
371
|
+
plan.bodyParam = {
|
|
372
|
+
asyncType: "&std::collections::HashMap<String, String>",
|
|
373
|
+
local: bodyLocal,
|
|
374
|
+
syncType: "&std::collections::HashMap<String, String>",
|
|
375
|
+
}
|
|
376
|
+
break
|
|
377
|
+
case "multipart":
|
|
378
|
+
/* multipart Form is consumed (not Clone), passed by value */
|
|
379
|
+
plan.bodyParam = {
|
|
380
|
+
asyncType: "reqwest::multipart::Form",
|
|
381
|
+
local: bodyLocal,
|
|
382
|
+
syncType: "reqwest::blocking::multipart::Form",
|
|
383
|
+
}
|
|
384
|
+
break
|
|
385
|
+
case "binary":
|
|
386
|
+
/* octet-stream body: generic over any byte stream / reader; single-shot, not retried on 401 */
|
|
387
|
+
plan.bodyParam = {
|
|
388
|
+
asyncType: "S",
|
|
389
|
+
generic: {
|
|
390
|
+
async: `S`,
|
|
391
|
+
asyncWhere: ` where S: futures::TryStream + Send + 'static, S::Error: Into<Box<dyn std::error::Error + Send + Sync>>, bytes::Bytes: From<S::Ok>`,
|
|
392
|
+
sync: `R: std::io::Read + Send + 'static`,
|
|
393
|
+
},
|
|
394
|
+
local: bodyLocal,
|
|
395
|
+
syncType: "R",
|
|
396
|
+
}
|
|
397
|
+
break
|
|
398
|
+
case "text":
|
|
399
|
+
plan.bodyParam = { asyncType: "&str", local: bodyLocal, syncType: "&str" }
|
|
400
|
+
break
|
|
334
401
|
}
|
|
335
|
-
for (const [, childNs] of namespacesOf(cur)) walk(childNs)
|
|
336
402
|
}
|
|
337
|
-
walk(ns)
|
|
338
|
-
return results
|
|
339
|
-
}
|
|
340
403
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
404
|
+
if (op.stream === null) {
|
|
405
|
+
const s = op.success
|
|
406
|
+
if (s.kind === "json") {
|
|
407
|
+
if (!s.schema) plan.resultType = "serde_json::Value"
|
|
408
|
+
else if (s.schema.kind === "ref") plan.resultType = names.types.ref(s.schema.name)
|
|
409
|
+
else {
|
|
410
|
+
const t = irRenderUseRust(s.schema, {
|
|
411
|
+
decls: names.decls,
|
|
412
|
+
fieldName: "Response",
|
|
413
|
+
names: names.types,
|
|
414
|
+
parentName: opPascal,
|
|
415
|
+
})
|
|
416
|
+
plan.resultType = t.startsWith("Box<") ? t.slice(4, -1) : t
|
|
417
|
+
}
|
|
418
|
+
} else if (s.kind === "text") plan.resultType = "String"
|
|
419
|
+
else if (s.kind === "binary") plan.resultType = "Vec<u8>"
|
|
420
|
+
/* the typed error enums exist in both modes; only safe-mode methods return them */
|
|
421
|
+
plan.envelopes = errorEnvelopes(op, opPascal, model, names)
|
|
422
|
+
if (plan.envelopes.length > 0) plan.errorEnum = names.scope.claim(`${opPascal}Error`)
|
|
423
|
+
}
|
|
424
|
+
void model
|
|
425
|
+
return plan
|
|
347
426
|
}
|
|
348
427
|
|
|
349
|
-
function emitOptsStruct(
|
|
428
|
+
function emitOptsStruct(plan: RustOpPlan): string[] {
|
|
350
429
|
const l: string[] = []
|
|
351
|
-
const optsName =
|
|
352
|
-
const decls = new Map<string, string>()
|
|
430
|
+
const { op, optsName } = plan
|
|
353
431
|
const fields: string[] = []
|
|
354
|
-
|
|
355
|
-
const emitted = new Set<string>()
|
|
356
|
-
for (const qp of m.queryParams) {
|
|
357
|
-
const fieldIdent = rustIdent(qp.name)
|
|
358
|
-
emitted.add(fieldIdent)
|
|
359
|
-
const rustType = renderUseRust(qp.schema, {
|
|
360
|
-
decls,
|
|
361
|
-
fieldName: qp.name,
|
|
362
|
-
parentName: optsName,
|
|
363
|
-
})
|
|
364
|
-
const finalType = qp.required ? rustType : `Option<${rustType}>`
|
|
365
|
-
const needsRename = fieldIdent !== qp.name
|
|
366
|
-
const needsSkip = !qp.required
|
|
367
|
-
if (needsRename && needsSkip) {
|
|
368
|
-
fields.push(`\t#[serde(rename = ${JSON.stringify(qp.name)}, skip_serializing_if = "Option::is_none")]`)
|
|
369
|
-
} else if (needsRename) {
|
|
370
|
-
fields.push(`\t#[serde(rename = ${JSON.stringify(qp.name)})]`)
|
|
371
|
-
} else if (needsSkip) {
|
|
372
|
-
fields.push(`\t#[serde(skip_serializing_if = "Option::is_none")]`)
|
|
373
|
-
}
|
|
374
|
-
fields.push(`\tpub ${fieldIdent}: ${finalType},`)
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (m.isSSE && !emitted.has("last_event_id")) {
|
|
378
|
-
fields.push(`\tpub last_event_id: Option<String>,`)
|
|
379
|
-
}
|
|
380
|
-
if (m.isWS && !emitted.has("reconnect_token")) {
|
|
432
|
+
if (op.stream === "realtime") {
|
|
381
433
|
fields.push(`\tpub reconnect_token: Option<String>,`)
|
|
382
|
-
|
|
383
|
-
if (m.isWS && !emitted.has("protocols")) {
|
|
434
|
+
fields.push(`\tpub last_event_id: Option<String>,`)
|
|
384
435
|
fields.push(`\tpub protocols: Option<Vec<String>>,`)
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
/* idempotency-key auto-injection opt-in field (9.a.5). Shared between async + sync
|
|
388
|
-
* method bodies; emitted once per opts struct. Not meaningful for SSE/WS/realtime. */
|
|
389
|
-
if (m.isIdempotent && !m.isSSE && !m.isWS && !m.isRealtime && !emitted.has("idempotency_key")) {
|
|
390
|
-
fields.push(`\t#[serde(skip_serializing_if = "Option::is_none")]`)
|
|
391
|
-
fields.push(`\tpub idempotency_key: Option<String>,`)
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/* per-call headers for all non-WS methods (SSE included) */
|
|
395
|
-
if (!m.isWS && !emitted.has("headers")) {
|
|
436
|
+
fields.push(`\tpub max_reconnect_attempts: Option<u32>,`)
|
|
437
|
+
fields.push(`\tpub reconnect_delay_ms: Option<u64>,`)
|
|
396
438
|
fields.push(`\t#[serde(skip)]`)
|
|
397
439
|
fields.push(`\tpub headers: Option<std::collections::HashMap<String, String>>,`)
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (
|
|
440
|
+
} else {
|
|
441
|
+
for (const f of plan.optFields) {
|
|
442
|
+
const bare = f.ident.startsWith("r#") ? f.ident.slice(2) : f.ident
|
|
443
|
+
const attrs: string[] = []
|
|
444
|
+
if (bare !== f.wire) attrs.push(`rename = ${rustString(f.wire)}`)
|
|
445
|
+
if (!f.required) attrs.push(`skip_serializing_if = "Option::is_none"`)
|
|
446
|
+
if (attrs.length > 0) fields.push(`\t#[serde(${attrs.join(", ")})]`)
|
|
447
|
+
fields.push(`\tpub ${f.ident}: ${f.type},`)
|
|
448
|
+
}
|
|
449
|
+
if (op.stream === "sse") fields.push(`\tpub last_event_id: Option<String>,`)
|
|
450
|
+
if (op.stream === "ws") {
|
|
451
|
+
fields.push(`\tpub reconnect_token: Option<String>,`)
|
|
452
|
+
fields.push(`\tpub protocols: Option<Vec<String>>,`)
|
|
453
|
+
}
|
|
454
|
+
if (op.idempotent && op.stream === null) {
|
|
455
|
+
fields.push(`\t#[serde(skip_serializing_if = "Option::is_none")]`)
|
|
456
|
+
fields.push(`\tpub idempotency_key: Option<String>,`)
|
|
457
|
+
}
|
|
458
|
+
/* per-call headers for all non-WS methods (SSE included) */
|
|
459
|
+
if (op.stream !== "ws") {
|
|
413
460
|
fields.push(`\t#[serde(skip)]`)
|
|
414
|
-
fields.push(`\tpub
|
|
461
|
+
fields.push(`\tpub headers: Option<std::collections::HashMap<String, String>>,`)
|
|
415
462
|
}
|
|
416
|
-
if (
|
|
463
|
+
if (op.stream === null) {
|
|
464
|
+
fields.push(`\t#[serde(skip)]`)
|
|
465
|
+
fields.push(`\tpub timeout: Option<std::time::Duration>,`)
|
|
466
|
+
/* async consumes `cancel_token`, sync consumes `sync_cancel_token`; both runtime-only */
|
|
467
|
+
fields.push(`\t#[serde(skip)]`)
|
|
468
|
+
fields.push(`\tpub cancel_token: Option<tokio_util::sync::CancellationToken>,`)
|
|
417
469
|
fields.push(`\t#[serde(skip)]`)
|
|
418
470
|
fields.push(`\tpub sync_cancel_token: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,`)
|
|
419
471
|
}
|
|
420
472
|
}
|
|
421
|
-
|
|
422
|
-
for (const decl of decls.values()) {
|
|
423
|
-
l.push(decl)
|
|
424
|
-
l.push(``)
|
|
425
|
-
}
|
|
426
|
-
|
|
427
473
|
l.push(`#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]`)
|
|
428
474
|
l.push(`pub struct ${optsName} {`)
|
|
429
475
|
l.push(...fields)
|
|
@@ -432,681 +478,478 @@ function emitOptsStruct(m: MethodInfo): string[] {
|
|
|
432
478
|
return l
|
|
433
479
|
}
|
|
434
480
|
|
|
435
|
-
function
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
optsName: string,
|
|
442
|
-
l: string[],
|
|
443
|
-
): string[] {
|
|
444
|
-
params.push(`opts: &${optsName}`)
|
|
445
|
-
l.push(`pub fn ${actionFn}(${params.join(", ")}) -> impl Stream<Item = Result<SseEvent, Error>> {`)
|
|
446
|
-
/* clone all opts fields into owned locals before the stream block to avoid capturing &opts lifetime */
|
|
447
|
-
l.push(`\tlet last_event_id_owned = opts.last_event_id.clone();`)
|
|
448
|
-
l.push(`\tlet opts_headers_owned = opts.headers.clone();`)
|
|
449
|
-
for (const qp of m.queryParams) {
|
|
450
|
-
if (qp.name === "last-event-id") continue
|
|
451
|
-
const fieldIdent = rustIdent(qp.name)
|
|
452
|
-
l.push(`\tlet ${fieldIdent}_owned = opts.${fieldIdent}.clone();`)
|
|
453
|
-
}
|
|
454
|
-
const urlExpr = buildUrlExpr(m.path, m.pathParams)
|
|
455
|
-
l.push(`\tlet url_path = ${urlExpr};`)
|
|
456
|
-
const sseQueryMut = m.queryParams.filter((q) => q.name !== "last-event-id").length > 0 ? "mut " : ""
|
|
457
|
-
l.push(`\tlet ${sseQueryMut}query: Vec<(String, String)> = Vec::new();`)
|
|
458
|
-
for (const qp of m.queryParams) {
|
|
459
|
-
if (qp.name === "last-event-id") continue
|
|
460
|
-
const fieldIdent = rustIdent(qp.name)
|
|
461
|
-
if (qp.required) {
|
|
462
|
-
l.push(`\tquery.push((${JSON.stringify(qp.name)}.to_string(), ${fieldIdent}_owned.clone()));`)
|
|
463
|
-
} else {
|
|
464
|
-
l.push(
|
|
465
|
-
`\tif let Some(ref v) = ${fieldIdent}_owned { query.push((${JSON.stringify(qp.name)}.to_string(), v.clone())); }`,
|
|
466
|
-
)
|
|
467
|
-
}
|
|
481
|
+
function docLines(op: SdkOp): string[] {
|
|
482
|
+
const l: string[] = []
|
|
483
|
+
if (op.summary) l.push(...rustDoc(op.summary))
|
|
484
|
+
if (op.description) {
|
|
485
|
+
if (op.summary) l.push(`///`)
|
|
486
|
+
l.push(...rustDoc(op.description))
|
|
468
487
|
}
|
|
469
|
-
l.push(
|
|
470
|
-
l.push(`\tlet client_clone = ${httpClientAccess}.clone();`)
|
|
471
|
-
l.push(`\tasync_stream::stream! {`)
|
|
472
|
-
l.push(`\t\tuse crate::sse::parse_sse_stream;`)
|
|
473
|
-
l.push(`\t\tlet mut req_headers = std::collections::HashMap::new();`)
|
|
474
|
-
l.push(`\t\treq_headers.insert("Accept".to_string(), "text/event-stream".to_string());`)
|
|
475
|
-
l.push(`\t\tif let Some(ref lei) = last_event_id_owned {`)
|
|
476
|
-
l.push(`\t\t\treq_headers.insert("Last-Event-ID".to_string(), lei.clone());`)
|
|
477
|
-
l.push(`\t\t}`)
|
|
478
|
-
l.push(`\t\tfor (k, v) in &cfg_clone.headers {`)
|
|
479
|
-
l.push(`\t\t\treq_headers.insert(k.clone(), v.clone());`)
|
|
480
|
-
l.push(`\t\t}`)
|
|
481
|
-
l.push(`\t\tif let Some(ref oh) = opts_headers_owned {`)
|
|
482
|
-
l.push(`\t\t\tfor (k, v) in oh { req_headers.insert(k.clone(), v.clone()); }`)
|
|
483
|
-
l.push(`\t\t}`)
|
|
484
|
-
l.push(`\t\tlet mut req = client_clone`)
|
|
485
|
-
l.push(`\t\t\t.get(format!("{}{}", cfg_clone.base_url, url_path))`)
|
|
486
|
-
l.push(`\t\t\t.header("Accept", "text/event-stream");`)
|
|
487
|
-
l.push(`\t\tif let Some(ref lei) = last_event_id_owned {`)
|
|
488
|
-
l.push(`\t\t\treq = req.header("Last-Event-ID", lei.clone());`)
|
|
489
|
-
l.push(`\t\t}`)
|
|
490
|
-
l.push(`\t\tmatch req.send().await {`)
|
|
491
|
-
l.push(`\t\t\tErr(e) => { yield Err(Error::Transport(e)); return; }`)
|
|
492
|
-
l.push(`\t\t\tOk(r) => {`)
|
|
493
|
-
l.push(`\t\t\t\tuse futures_util::StreamExt;`)
|
|
494
|
-
l.push(`\t\t\t\tlet mut s = std::pin::pin!(parse_sse_stream(r));`)
|
|
495
|
-
l.push(`\t\t\t\twhile let Some(item) = s.next().await { yield item; }`)
|
|
496
|
-
l.push(`\t\t\t}`)
|
|
497
|
-
l.push(`\t\t}`)
|
|
498
|
-
l.push(`\t}`)
|
|
499
|
-
l.push(`}`)
|
|
500
|
-
l.push(``)
|
|
488
|
+
if (op.deprecated) l.push(`#[deprecated(note = "the API marks this operation deprecated")]`)
|
|
501
489
|
return l
|
|
502
490
|
}
|
|
503
491
|
|
|
504
|
-
function
|
|
505
|
-
|
|
506
|
-
params
|
|
507
|
-
|
|
508
|
-
actionFn: string,
|
|
509
|
-
optsName: string,
|
|
510
|
-
l: string[],
|
|
511
|
-
): string[] {
|
|
512
|
-
params.push(`opts: &${optsName}`)
|
|
513
|
-
l.push(`pub async fn ${actionFn}(${params.join(", ")}) -> Result<TypedWebSocket, Error> {`)
|
|
514
|
-
const urlExpr = buildUrlExpr(m.path, m.pathParams)
|
|
515
|
-
l.push(`\tlet url_path = ${urlExpr};`)
|
|
516
|
-
l.push(`\tlet mut query_str = String::new();`)
|
|
517
|
-
for (const qp of m.queryParams) {
|
|
518
|
-
if (qp.name === "reconnect_token") continue
|
|
519
|
-
const fieldIdent = rustIdent(qp.name)
|
|
520
|
-
if (qp.required) {
|
|
521
|
-
l.push(`\tif !query_str.is_empty() { query_str.push('&'); }`)
|
|
522
|
-
l.push(
|
|
523
|
-
`\tquery_str.push_str(&format!("{}={}", ${JSON.stringify(qp.name)}, urlencoding::encode(&opts.${fieldIdent})));`,
|
|
524
|
-
)
|
|
525
|
-
} else {
|
|
526
|
-
l.push(`\tif let Some(ref v) = opts.${fieldIdent} {`)
|
|
527
|
-
l.push(`\t\tif !query_str.is_empty() { query_str.push('&'); }`)
|
|
528
|
-
l.push(`\t\tquery_str.push_str(&format!("{}={}", ${JSON.stringify(qp.name)}, urlencoding::encode(v)));`)
|
|
529
|
-
l.push(`\t}`)
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
l.push(`\tif let Some(ref rt) = opts.reconnect_token {`)
|
|
533
|
-
l.push(`\t\tif !query_str.is_empty() { query_str.push('&'); }`)
|
|
534
|
-
l.push(`\t\tquery_str.push_str(&format!("reconnect_token={}", urlencoding::encode(rt)));`)
|
|
535
|
-
l.push(`\t}`)
|
|
536
|
-
l.push(`\tlet full_url = if query_str.is_empty() {`)
|
|
537
|
-
l.push(`\t\tformat!("{}{}", ${cfgAccess}.base_url, url_path)`)
|
|
538
|
-
l.push(`\t} else {`)
|
|
539
|
-
l.push(`\t\tformat!("{}{}?{}", ${cfgAccess}.base_url, url_path, query_str)`)
|
|
540
|
-
l.push(`\t};`)
|
|
541
|
-
l.push(`\t/* convert http(s) → ws(s) */`)
|
|
542
|
-
l.push(`\tlet ws_url = full_url.replacen("https://", "wss://", 1).replacen("http://", "ws://", 1);`)
|
|
543
|
-
l.push(`\tuse tokio_tungstenite::tungstenite::client::IntoClientRequest;`)
|
|
544
|
-
l.push(`\tlet mut req = ws_url.as_str().into_client_request()`)
|
|
545
|
-
l.push(`\t\t.map_err(|e| Error::Other(e.to_string()))?;`)
|
|
546
|
-
l.push(`\tif let Some(ref protos) = opts.protocols {`)
|
|
547
|
-
l.push(`\t\tif !protos.is_empty() {`)
|
|
548
|
-
l.push(`\t\t\treq.headers_mut().insert(`)
|
|
549
|
-
l.push(`\t\t\t\t"Sec-WebSocket-Protocol",`)
|
|
550
|
-
l.push(
|
|
551
|
-
`\t\t\t\tprotos.join(", ").parse().map_err(|e: tokio_tungstenite::tungstenite::http::header::InvalidHeaderValue| Error::Other(e.to_string()))?,`,
|
|
552
|
-
)
|
|
553
|
-
l.push(`\t\t\t);`)
|
|
554
|
-
l.push(`\t\t}`)
|
|
555
|
-
l.push(`\t}`)
|
|
556
|
-
l.push(`\tlet (stream, _) = tokio_tungstenite::connect_async(req).await`)
|
|
557
|
-
l.push(`\t\t.map_err(|e| Error::Other(e.to_string()))?;`)
|
|
558
|
-
l.push(`\tOk(TypedWebSocket::new(stream))`)
|
|
559
|
-
l.push(`}`)
|
|
560
|
-
l.push(``)
|
|
561
|
-
return l
|
|
492
|
+
function pathLines(plan: RustOpPlan): string[] {
|
|
493
|
+
if (plan.pathParams.length === 0) return [`\tlet url_path = ${rustString(plan.op.path)}.to_string();`]
|
|
494
|
+
const params = plan.pathParams.map((p) => `(${rustString(p.wire)}, ${p.local})`).join(", ")
|
|
495
|
+
return [`\tlet url_path = crate::runtime::expand_path(${rustString(plan.op.path)}, &[${params}])?;`]
|
|
562
496
|
}
|
|
563
497
|
|
|
564
|
-
function
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
498
|
+
function queryLines(plan: RustOpPlan, opts = "opts"): string[] {
|
|
499
|
+
const q = plan.optFields.filter((f) => f.where === "query")
|
|
500
|
+
const l = [`\tlet ${q.length > 0 ? "mut " : ""}query: Vec<(String, String)> = Vec::new();`]
|
|
501
|
+
for (const f of q)
|
|
502
|
+
l.push(`\tcrate::runtime::push_serialized(${rustString(f.wire)}, &${opts}.${f.ident}, &mut query);`)
|
|
503
|
+
return l
|
|
568
504
|
}
|
|
569
505
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
l.push(
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const params: string[] = [`&self`]
|
|
582
|
-
for (const pp of m.pathParams) {
|
|
583
|
-
params.push(`${rustSnake(pp)}: &str`)
|
|
584
|
-
}
|
|
585
|
-
/* body param depends on body kind; `asyncGenerics` collects generic parameter
|
|
586
|
-
* declarations for the function signature (only stream bodies need generics
|
|
587
|
-
* for zero-cost monomorphization over any `Stream<Bytes>`). */
|
|
588
|
-
const asyncGenerics: string[] = []
|
|
589
|
-
const bi = m.bodyInfo
|
|
590
|
-
if (bi.kind === "json") {
|
|
591
|
-
params.push(`body: &${bi.type}`)
|
|
592
|
-
} else if (bi.kind === "form-url") {
|
|
593
|
-
params.push(`body: &std::collections::HashMap<String, String>`)
|
|
594
|
-
} else if (bi.kind === "multipart") {
|
|
595
|
-
/* multipart Form is consumed (not Clone), passed by value */
|
|
596
|
-
params.push(`body: reqwest::multipart::Form`)
|
|
597
|
-
} else if (bi.kind === "stream") {
|
|
598
|
-
/* octet-stream body: generic over any `Stream<Item = Result<Bytes, reqwest::Error>>`
|
|
599
|
-
* (reqwest::Body::wrap_stream bound). Owned body, single-shot, non-retryable on 401. */
|
|
600
|
-
asyncGenerics.push(`S: futures::Stream<Item = Result<bytes::Bytes, reqwest::Error>> + Send + Sync + 'static`)
|
|
601
|
-
params.push(`body: S`)
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
const { cfg: cfgAccess, stale: staleAccess, httpClient: httpClientAccess } = buildAccessors(selfAccess)
|
|
605
|
-
|
|
606
|
-
if (m.isSSE) {
|
|
607
|
-
params.push(`opts: &${optsName}`)
|
|
608
|
-
l.push(`pub fn ${actionFn}(${params.join(", ")}) -> impl Stream<Item = Result<SseEvent, Error>> {`)
|
|
609
|
-
/* clone all opts fields into owned locals before the stream block to avoid capturing &opts lifetime */
|
|
610
|
-
l.push(`\tlet last_event_id_owned = opts.last_event_id.clone();`)
|
|
611
|
-
l.push(`\tlet opts_headers_owned = opts.headers.clone();`)
|
|
612
|
-
for (const qp of m.queryParams) {
|
|
613
|
-
if (qp.name === "last-event-id") continue
|
|
614
|
-
const fieldIdent = rustIdent(qp.name)
|
|
615
|
-
l.push(`\tlet ${fieldIdent}_owned = opts.${fieldIdent}.clone();`)
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const urlExpr = buildUrlExpr(m.path, m.pathParams)
|
|
619
|
-
l.push(`\tlet url_path = ${urlExpr};`)
|
|
620
|
-
const sseQueryMut = m.queryParams.filter((q) => q.name !== "last-event-id").length > 0 ? "mut " : ""
|
|
621
|
-
l.push(`\tlet ${sseQueryMut}query: Vec<(String, String)> = Vec::new();`)
|
|
622
|
-
for (const qp of m.queryParams) {
|
|
623
|
-
if (qp.name === "last-event-id") continue
|
|
624
|
-
const fieldIdent = rustIdent(qp.name)
|
|
625
|
-
if (qp.required) {
|
|
626
|
-
l.push(`\tquery.push((${JSON.stringify(qp.name)}.to_string(), ${fieldIdent}_owned.clone()));`)
|
|
627
|
-
} else {
|
|
628
|
-
l.push(
|
|
629
|
-
`\tif let Some(ref v) = ${fieldIdent}_owned { query.push((${JSON.stringify(qp.name)}.to_string(), v.clone())); }`,
|
|
630
|
-
)
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
l.push(`\tlet cfg_clone = ${cfgAccess}.clone();`)
|
|
634
|
-
l.push(`\tlet client_clone = ${httpClientAccess}.clone();`)
|
|
635
|
-
l.push(`\tasync_stream::stream! {`)
|
|
636
|
-
l.push(`\t\tuse crate::sse::parse_sse_stream;`)
|
|
637
|
-
l.push(`\t\tlet mut req_headers = std::collections::HashMap::new();`)
|
|
638
|
-
l.push(`\t\treq_headers.insert("Accept".to_string(), "text/event-stream".to_string());`)
|
|
639
|
-
l.push(`\t\tif let Some(ref lei) = last_event_id_owned {`)
|
|
640
|
-
l.push(`\t\t\treq_headers.insert("Last-Event-ID".to_string(), lei.clone());`)
|
|
641
|
-
l.push(`\t\t}`)
|
|
642
|
-
l.push(`\t\tfor (k, v) in &cfg_clone.headers {`)
|
|
643
|
-
l.push(`\t\t\treq_headers.insert(k.clone(), v.clone());`)
|
|
644
|
-
l.push(`\t\t}`)
|
|
645
|
-
l.push(`\t\tif let Some(ref oh) = opts_headers_owned {`)
|
|
646
|
-
l.push(`\t\t\tfor (k, v) in oh { req_headers.insert(k.clone(), v.clone()); }`)
|
|
647
|
-
l.push(`\t\t}`)
|
|
648
|
-
l.push(`\t\tlet mut req = client_clone`)
|
|
649
|
-
l.push(`\t\t\t.get(format!("{}{}", cfg_clone.base_url, url_path))`)
|
|
650
|
-
l.push(`\t\t\t.header("Accept", "text/event-stream");`)
|
|
651
|
-
l.push(`\t\tif let Some(ref lei) = last_event_id_owned {`)
|
|
652
|
-
l.push(`\t\t\treq = req.header("Last-Event-ID", lei.clone());`)
|
|
653
|
-
l.push(`\t\t}`)
|
|
654
|
-
l.push(`\t\tmatch req.send().await {`)
|
|
655
|
-
l.push(`\t\t\tErr(e) => { yield Err(Error::Transport(e)); return; }`)
|
|
656
|
-
l.push(`\t\t\tOk(r) => {`)
|
|
657
|
-
l.push(`\t\t\t\tuse futures_util::StreamExt;`)
|
|
658
|
-
l.push(`\t\t\t\tlet mut s = std::pin::pin!(parse_sse_stream(r));`)
|
|
659
|
-
l.push(`\t\t\t\twhile let Some(item) = s.next().await { yield item; }`)
|
|
660
|
-
l.push(`\t\t\t}`)
|
|
661
|
-
l.push(`\t\t}`)
|
|
662
|
-
l.push(`\t}`)
|
|
663
|
-
l.push(`}`)
|
|
664
|
-
l.push(``)
|
|
665
|
-
return l
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
if (m.isWS) {
|
|
669
|
-
params.push(`opts: &${optsName}`)
|
|
670
|
-
l.push(`pub async fn ${actionFn}(${params.join(", ")}) -> Result<TypedWebSocket, Error> {`)
|
|
671
|
-
const urlExpr = buildUrlExpr(m.path, m.pathParams)
|
|
672
|
-
l.push(`\tlet url_path = ${urlExpr};`)
|
|
673
|
-
l.push(`\tlet mut query_str = String::new();`)
|
|
674
|
-
for (const qp of m.queryParams) {
|
|
675
|
-
if (qp.name === "reconnect_token") continue
|
|
676
|
-
const fieldIdent = rustIdent(qp.name)
|
|
677
|
-
if (qp.required) {
|
|
678
|
-
l.push(`\tif !query_str.is_empty() { query_str.push('&'); }`)
|
|
679
|
-
l.push(
|
|
680
|
-
`\tquery_str.push_str(&format!("{}={}", ${JSON.stringify(qp.name)}, urlencoding::encode(&opts.${fieldIdent})));`,
|
|
681
|
-
)
|
|
682
|
-
} else {
|
|
683
|
-
l.push(`\tif let Some(ref v) = opts.${fieldIdent} {`)
|
|
684
|
-
l.push(`\t\tif !query_str.is_empty() { query_str.push('&'); }`)
|
|
685
|
-
l.push(`\t\tquery_str.push_str(&format!("{}={}", ${JSON.stringify(qp.name)}, urlencoding::encode(v)));`)
|
|
686
|
-
l.push(`\t}`)
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
l.push(`\tif let Some(ref rt) = opts.reconnect_token {`)
|
|
690
|
-
l.push(`\t\tif !query_str.is_empty() { query_str.push('&'); }`)
|
|
691
|
-
l.push(`\t\tquery_str.push_str(&format!("reconnect_token={}", urlencoding::encode(rt)));`)
|
|
692
|
-
l.push(`\t}`)
|
|
693
|
-
l.push(`\tlet full_url = if query_str.is_empty() {`)
|
|
694
|
-
l.push(`\t\tformat!("{}{}", ${cfgAccess}.base_url, url_path)`)
|
|
695
|
-
l.push(`\t} else {`)
|
|
696
|
-
l.push(`\t\tformat!("{}{}?{}", ${cfgAccess}.base_url, url_path, query_str)`)
|
|
697
|
-
l.push(`\t};`)
|
|
698
|
-
l.push(`\t/* convert http(s) → ws(s) */`)
|
|
699
|
-
l.push(`\tlet ws_url = full_url.replacen("https://", "wss://", 1).replacen("http://", "ws://", 1);`)
|
|
700
|
-
l.push(`\tuse tokio_tungstenite::tungstenite::client::IntoClientRequest;`)
|
|
701
|
-
l.push(`\tlet mut req = ws_url.as_str().into_client_request()`)
|
|
702
|
-
l.push(`\t\t.map_err(|e| Error::Other(e.to_string()))?;`)
|
|
703
|
-
l.push(`\tif let Some(ref protos) = opts.protocols {`)
|
|
704
|
-
l.push(`\t\tif !protos.is_empty() {`)
|
|
705
|
-
l.push(`\t\t\treq.headers_mut().insert(`)
|
|
706
|
-
l.push(`\t\t\t\t"Sec-WebSocket-Protocol",`)
|
|
506
|
+
/** Lines that build `call_headers` from opts.headers, header params, then `extra` entries. */
|
|
507
|
+
function headerLines(plan: RustOpPlan, extra: string[] = []): string[] {
|
|
508
|
+
const l = [
|
|
509
|
+
`\tlet mut call_headers: std::collections::HashMap<String, String> = opts.headers.clone().unwrap_or_default();`,
|
|
510
|
+
]
|
|
511
|
+
for (const f of plan.optFields.filter((x) => x.where === "header")) {
|
|
512
|
+
l.push(`\t{`)
|
|
513
|
+
l.push(`\t\tlet mut values: Vec<(String, String)> = Vec::new();`)
|
|
514
|
+
l.push(`\t\tcrate::runtime::push_serialized(${rustString(f.wire)}, &opts.${f.ident}, &mut values);`)
|
|
515
|
+
l.push(`\t\tif !values.is_empty() {`)
|
|
707
516
|
l.push(
|
|
708
|
-
`\t\t\
|
|
517
|
+
`\t\t\tcrate::runtime::set_header(&mut call_headers, ${rustString(f.wire)}, values.into_iter().map(|(_, v)| v).collect::<Vec<_>>().join(","));`,
|
|
709
518
|
)
|
|
710
|
-
l.push(`\t\t\t);`)
|
|
711
519
|
l.push(`\t\t}`)
|
|
712
520
|
l.push(`\t}`)
|
|
713
|
-
l.push(`\tlet (stream, _) = tokio_tungstenite::connect_async(req).await`)
|
|
714
|
-
l.push(`\t\t.map_err(|e| Error::Other(e.to_string()))?;`)
|
|
715
|
-
l.push(`\tOk(TypedWebSocket::new(stream))`)
|
|
716
|
-
l.push(`}`)
|
|
717
|
-
l.push(``)
|
|
718
|
-
return l
|
|
719
521
|
}
|
|
522
|
+
l.push(...extra)
|
|
523
|
+
return l
|
|
524
|
+
}
|
|
720
525
|
|
|
721
|
-
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
526
|
+
function bodyExpr(plan: RustOpPlan, sync: boolean): { lines: string[]; expr: string } {
|
|
527
|
+
const prefix = sync ? "crate::runtime_sync::SyncRequestBody" : "crate::runtime::RequestBody"
|
|
528
|
+
const b = plan.op.body
|
|
529
|
+
const local = plan.bodyParam?.local
|
|
530
|
+
if (!b || !local) return { expr: `${prefix}::None`, lines: [] }
|
|
531
|
+
switch (b.kind) {
|
|
532
|
+
case "json":
|
|
533
|
+
return { expr: `${prefix}::Json(&body_value)`, lines: [`\tlet body_value = serde_json::to_value(${local})?;`] }
|
|
534
|
+
case "form":
|
|
535
|
+
return { expr: `${prefix}::FormUrl(crate::runtime::form_pairs(${local}))`, lines: [] }
|
|
536
|
+
case "multipart":
|
|
537
|
+
return { expr: `${prefix}::Multipart(${local})`, lines: [] }
|
|
538
|
+
case "binary":
|
|
539
|
+
return {
|
|
540
|
+
expr: sync
|
|
541
|
+
? `${prefix}::Stream(reqwest::blocking::Body::new(${local}))`
|
|
542
|
+
: `${prefix}::Stream(reqwest::Body::wrap_stream(${local}))`,
|
|
543
|
+
lines: [],
|
|
544
|
+
}
|
|
545
|
+
case "text":
|
|
546
|
+
return {
|
|
547
|
+
expr: `${prefix}::Raw(${rustString(b.contentType)}, bytes::Bytes::from(${local}.to_string()))`,
|
|
548
|
+
lines: [],
|
|
549
|
+
}
|
|
732
550
|
}
|
|
551
|
+
}
|
|
733
552
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
if (bi.kind === "json") {
|
|
747
|
-
bodyArg = `crate::runtime::RequestBody::Json(&serde_json::to_value(body).unwrap_or_default())`
|
|
748
|
-
} else if (bi.kind === "form-url") {
|
|
749
|
-
bodyArg = `crate::runtime::RequestBody::FormUrl(body)`
|
|
750
|
-
} else if (bi.kind === "multipart") {
|
|
751
|
-
bodyArg = `crate::runtime::RequestBody::Multipart(body)`
|
|
752
|
-
} else if (bi.kind === "stream") {
|
|
753
|
-
bodyArg = `crate::runtime::RequestBody::Stream(reqwest::Body::wrap_stream(body))`
|
|
754
|
-
} else {
|
|
755
|
-
bodyArg = `crate::runtime::RequestBody::None`
|
|
553
|
+
function decodeLines(plan: RustOpPlan, throwOnError: boolean): string[] {
|
|
554
|
+
switch (plan.op.success.kind) {
|
|
555
|
+
case "text":
|
|
556
|
+
return [`\tlet out: String = String::from_utf8_lossy(&result.body).into_owned();`]
|
|
557
|
+
case "binary":
|
|
558
|
+
/* the bytes move into the result; only a safe-mode call keeps a second copy in `response` */
|
|
559
|
+
return [`\tlet out: Vec<u8> = ${throwOnError ? "result.body" : "result.body.clone()"};`]
|
|
560
|
+
default:
|
|
561
|
+
/* an empty JSON body decodes as `null` (Option / Value types accept it) */
|
|
562
|
+
return [
|
|
563
|
+
`\tlet out: ${plan.resultType} = serde_json::from_slice(if result.body.is_empty() { b"null" } else { &result.body })?;`,
|
|
564
|
+
]
|
|
756
565
|
}
|
|
566
|
+
}
|
|
757
567
|
|
|
758
|
-
|
|
759
|
-
* selector, snapshot RequestMeta from the tracker. Emitted only for ops
|
|
760
|
-
* with x-invalidate; BuildRequestMeta short-circuits to None when disabled. */
|
|
761
|
-
const hasInvalidate = m.invalidates.length > 0
|
|
762
|
-
if (hasInvalidate) {
|
|
763
|
-
const pathParamsMapExpr =
|
|
764
|
-
m.pathParams.length > 0
|
|
765
|
-
? `std::collections::HashMap::from([${m.pathParams.map((p) => `(${JSON.stringify(p)}.to_string(), ${rustSnake(p)}.to_string())`).join(", ")}])`
|
|
766
|
-
: `std::collections::HashMap::<String, String>::new()`
|
|
767
|
-
l.push(`\tlet _path_params_map: std::collections::HashMap<String, String> = ${pathParamsMapExpr};`)
|
|
768
|
-
l.push(
|
|
769
|
-
`\tlet _concrete_path = crate::invalidation::interpolate_path(${JSON.stringify(m.path)}, &_path_params_map).unwrap_or_else(|_| ${JSON.stringify(m.path)}.to_string());`,
|
|
770
|
-
)
|
|
771
|
-
l.push(`\tlet _concrete_selector = format!("${httpMethod} {}", _concrete_path);`)
|
|
772
|
-
l.push(`\tlet _request_meta: Option<crate::invalidation::RequestMeta> = match ${staleAccess} {`)
|
|
773
|
-
l.push(
|
|
774
|
-
`\t\tSome(ref s) => s.build_request_meta(&_concrete_selector, &_concrete_path, ${JSON.stringify(httpMethod)}).await,`,
|
|
775
|
-
)
|
|
776
|
-
l.push(`\t\tNone => None,`)
|
|
777
|
-
l.push(`\t};`)
|
|
778
|
-
}
|
|
568
|
+
type Access = { cfg: string; stale: string; http: string; auth: string }
|
|
779
569
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
* Matches TS/Python/Go emitter semantics (9.a.2 / 9.a.3 / 9.a.4). */
|
|
786
|
-
const emitIdem = m.isIdempotent && !m.isSSE && !m.isWS && !m.isRealtime
|
|
787
|
-
if (emitIdem) {
|
|
788
|
-
l.push(
|
|
789
|
-
`\tlet mut _call_headers: std::collections::HashMap<String, String> = opts.headers.clone().unwrap_or_default();`,
|
|
790
|
-
)
|
|
791
|
-
l.push(`\tlet _has_idem = _call_headers.keys().any(|k| k.eq_ignore_ascii_case("Idempotency-Key"));`)
|
|
792
|
-
l.push(`\tif !_has_idem {`)
|
|
793
|
-
l.push(`\t\tlet _k = opts.idempotency_key.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string());`)
|
|
794
|
-
l.push(`\t\t_call_headers.insert("Idempotency-Key".to_string(), _k);`)
|
|
795
|
-
l.push(`\t}`)
|
|
796
|
-
}
|
|
570
|
+
function access(selfAccess: "self" | "self.client"): Access {
|
|
571
|
+
return selfAccess === "self"
|
|
572
|
+
? { auth: "self.inner.auth", cfg: "self.inner.cfg", http: "self.inner.http_client", stale: "self.inner.stale" }
|
|
573
|
+
: { auth: "self.client.auth", cfg: "self.client.cfg", http: "self.client.http_client", stale: "self.client.stale" }
|
|
574
|
+
}
|
|
797
575
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
l
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
l.push(
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
l.push(`\
|
|
825
|
-
l.push(`\t\
|
|
576
|
+
function returnType(plan: RustOpPlan, throwOnError: boolean): string {
|
|
577
|
+
if (plan.resultType === "") return "Result<(), Error>"
|
|
578
|
+
if (throwOnError) return `Result<${plan.resultType}, Error>`
|
|
579
|
+
if (plan.errorEnum) return `Result<SdkResult<${plan.resultType}, ${plan.errorEnum}>, Error>`
|
|
580
|
+
return `Result<SdkResult<${plan.resultType}>, Error>`
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Shared tail: run the request (already bound to `result`), invalidate, decode. */
|
|
584
|
+
function resultLines(plan: RustOpPlan, throwOnError: boolean, sync: boolean, a: Access, call: string[]): string[] {
|
|
585
|
+
const l: string[] = []
|
|
586
|
+
const aw = sync ? "" : ".await"
|
|
587
|
+
const method = plan.op.method
|
|
588
|
+
|
|
589
|
+
/* invalidation pre-flight for every call: reads see and clear staleness too */
|
|
590
|
+
const pp = plan.pathParams.map((p) => `(${rustString(p.wire)}.to_string(), ${p.local}.to_string())`).join(", ")
|
|
591
|
+
l.push(
|
|
592
|
+
`\tlet path_params: std::collections::HashMap<String, String> = ${plan.pathParams.length > 0 ? `std::collections::HashMap::from([${pp}])` : "std::collections::HashMap::new()"};`,
|
|
593
|
+
)
|
|
594
|
+
l.push(`\tlet selector = format!("{} {}", ${rustString(method)}, url_path);`)
|
|
595
|
+
l.push(`\tlet request_meta: Option<crate::invalidation::RequestMeta> = match ${a.stale} {`)
|
|
596
|
+
l.push(`\t\tSome(ref s) => s.build_request_meta(&selector, &url_path, ${rustString(method)})${aw},`)
|
|
597
|
+
l.push(`\t\tNone => None,`)
|
|
598
|
+
l.push(`\t};`)
|
|
599
|
+
|
|
600
|
+
const safe = !throwOnError && plan.resultType !== ""
|
|
601
|
+
if (safe) {
|
|
602
|
+
l.push(`\tlet result = match ${call.join("\n\t\t")} {`)
|
|
603
|
+
l.push(`\t\tOk(r) => r,`)
|
|
604
|
+
l.push(`\t\tErr(Error::Api(api)) => {`)
|
|
605
|
+
l.push(`\t\t\tlet status = api.status();`)
|
|
606
|
+
if (plan.errorEnum) {
|
|
607
|
+
l.push(`\t\t\tlet body = api.body().to_vec();`)
|
|
608
|
+
l.push(`\t\t\tlet error = match status {`)
|
|
609
|
+
for (const env of plan.envelopes) {
|
|
610
|
+
l.push(`\t\t\t\t${env.status} => match serde_json::from_slice::<${env.structName}>(&body) {`)
|
|
611
|
+
l.push(`\t\t\t\t\tOk(v) => ${plan.errorEnum}::${env.variantName}(v),`)
|
|
612
|
+
l.push(`\t\t\t\t\tErr(_) => ${plan.errorEnum}::Other(Error::Api(api)),`)
|
|
613
|
+
l.push(`\t\t\t\t},`)
|
|
614
|
+
}
|
|
615
|
+
l.push(`\t\t\t\t_ => ${plan.errorEnum}::Other(Error::Api(api)),`)
|
|
616
|
+
l.push(`\t\t\t};`)
|
|
617
|
+
l.push(`\t\t\tlet mut response = crate::runtime::ResponseMeta::empty(status);`)
|
|
618
|
+
l.push(`\t\t\tresponse.body = body;`)
|
|
619
|
+
l.push(`\t\t\treturn Ok(SdkResult { data: None, error: Some(error), status, response });`)
|
|
620
|
+
} else {
|
|
621
|
+
l.push(`\t\t\tlet mut response = crate::runtime::ResponseMeta::empty(status);`)
|
|
622
|
+
l.push(`\t\t\tresponse.body = api.body().to_vec();`)
|
|
623
|
+
l.push(`\t\t\treturn Ok(SdkResult { data: None, error: Some(Error::Api(api)), status, response });`)
|
|
624
|
+
}
|
|
826
625
|
l.push(`\t\t}`)
|
|
827
|
-
l.push(`\t\
|
|
828
|
-
l.push(`\t
|
|
626
|
+
l.push(`\t\tErr(e) => return Err(e),`)
|
|
627
|
+
l.push(`\t};`)
|
|
628
|
+
} else {
|
|
629
|
+
l.push(`\tlet result = ${call.join("\n\t\t")}?;`)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/* invalidation post-flight: mark declared targets stale (templated ones expanded with this
|
|
633
|
+
* call's params), then clear this key if the call read through a stale window */
|
|
634
|
+
l.push(`\tif let Some(ref stale) = ${a.stale} {`)
|
|
635
|
+
if (plan.op.invalidates.length > 0) {
|
|
829
636
|
l.push(
|
|
830
|
-
`\t\
|
|
637
|
+
`\t\tlet targets: Vec<String> = vec![${plan.op.invalidates.map((t) => `${rustString(t)}.to_string()`).join(", ")}];`,
|
|
831
638
|
)
|
|
832
|
-
l.push(`\t\
|
|
833
|
-
l.push(`\t\t}`)
|
|
834
|
-
l.push(`\t}`)
|
|
639
|
+
l.push(`\t\tstale.mark_stale(&targets, &path_params, &selector)${aw};`)
|
|
835
640
|
}
|
|
641
|
+
l.push(`\t\tif let Some(ref meta) = request_meta {`)
|
|
642
|
+
l.push(`\t\t\tif meta.is_stale {`)
|
|
643
|
+
l.push(`\t\t\t\tstale.clear_stale(&selector, &url_path, ${rustString(method)}, meta.seq_snapshot)${aw};`)
|
|
644
|
+
l.push(`\t\t\t}`)
|
|
645
|
+
l.push(`\t\t}`)
|
|
646
|
+
l.push(`\t}`)
|
|
836
647
|
|
|
837
|
-
if (
|
|
648
|
+
if (plan.resultType === "") {
|
|
649
|
+
l.push(`\tlet _ = result;`)
|
|
838
650
|
l.push(`\tOk(())`)
|
|
839
|
-
l.push(`}`)
|
|
840
|
-
l.push(``)
|
|
841
651
|
return l
|
|
842
652
|
}
|
|
843
|
-
|
|
653
|
+
l.push(...decodeLines(plan, throwOnError))
|
|
844
654
|
if (throwOnError) {
|
|
845
|
-
l.push(`\tlet out: ${m.responseType} = serde_json::from_slice(&result.body)?;`)
|
|
846
655
|
l.push(`\tOk(out)`)
|
|
847
656
|
} else {
|
|
848
|
-
|
|
657
|
+
/* the response snapshot takes the body, headers and URL by move */
|
|
849
658
|
l.push(
|
|
850
|
-
`\tOk(SdkResult { data: Some(out), error: None, status: result.status, response: crate::runtime::ResponseMeta { status: result.status, headers: result.headers
|
|
659
|
+
`\tOk(SdkResult { data: Some(out), error: None, status: result.status, response: crate::runtime::ResponseMeta { status: result.status, headers: result.headers, url: result.url, body: result.body } })`,
|
|
851
660
|
)
|
|
852
661
|
}
|
|
853
|
-
|
|
854
|
-
l.push(`}`)
|
|
855
|
-
l.push(``)
|
|
856
662
|
return l
|
|
857
663
|
}
|
|
858
664
|
|
|
859
|
-
|
|
860
|
-
function emitSyncMethod(m: MethodInfo, selfAccess: "self" | "self.client", throwOnError: boolean): string[] {
|
|
861
|
-
/* SSE and WS operations have no sync variant */
|
|
862
|
-
if (m.isSSE || m.isWS) return []
|
|
863
|
-
|
|
864
|
-
const l: string[] = []
|
|
865
|
-
const actionFn = rustMethodIdent(m.action === "_call" ? m.resource : m.methodAction)
|
|
866
|
-
const optsName = optsStructName(m)
|
|
867
|
-
const httpMethod = m.httpMethod
|
|
868
|
-
|
|
869
|
-
if (m.summary) {
|
|
870
|
-
l.push(`/// ${m.summary}`)
|
|
871
|
-
if (m.description) l.push(`/// ${m.description}`)
|
|
872
|
-
}
|
|
873
|
-
|
|
665
|
+
function signature(plan: RustOpPlan, sync: boolean): { generics: string; params: string; where: string } {
|
|
874
666
|
const params: string[] = [`&self`]
|
|
875
|
-
for (const
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
} else if (bi.kind === "form-url") {
|
|
885
|
-
params.push(`body: &std::collections::HashMap<String, String>`)
|
|
886
|
-
} else if (bi.kind === "multipart") {
|
|
887
|
-
params.push(`body: reqwest::blocking::multipart::Form`)
|
|
888
|
-
} else if (bi.kind === "stream") {
|
|
889
|
-
syncGenerics.push(`R: std::io::Read + Send + 'static`)
|
|
890
|
-
params.push(`body: R`)
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
const { cfg: cfgAccess, stale: staleAccess, httpClient: httpClientAccess } = buildAccessors(selfAccess)
|
|
894
|
-
|
|
895
|
-
const opErrEnum = !throwOnError ? getOpErrorEnumName(m.opId, m.op) : null
|
|
896
|
-
let retType: string
|
|
897
|
-
if (throwOnError) {
|
|
898
|
-
retType = m.responseType !== "" ? `Result<${m.responseType}, Error>` : `Result<(), Error>`
|
|
899
|
-
} else if (opErrEnum && m.responseType !== "") {
|
|
900
|
-
retType = `Result<SdkResult<${m.responseType}, ${opErrEnum}>, Error>`
|
|
901
|
-
} else {
|
|
902
|
-
retType = m.responseType !== "" ? `Result<SdkResult<${m.responseType}>, Error>` : `Result<(), Error>`
|
|
667
|
+
for (const p of plan.pathParams) params.push(`${p.local}: &str`)
|
|
668
|
+
let generics = ""
|
|
669
|
+
let where = ""
|
|
670
|
+
if (plan.bodyParam) {
|
|
671
|
+
params.push(`${plan.bodyParam.local}: ${sync ? plan.bodyParam.syncType : plan.bodyParam.asyncType}`)
|
|
672
|
+
if (plan.bodyParam.generic) {
|
|
673
|
+
generics = `<${sync ? plan.bodyParam.generic.sync : plan.bodyParam.generic.async}>`
|
|
674
|
+
if (!sync) where = plan.bodyParam.generic.asyncWhere ?? ""
|
|
675
|
+
}
|
|
903
676
|
}
|
|
677
|
+
params.push(`opts: &${plan.optsName}`)
|
|
678
|
+
return { generics, params: params.join(", "), where }
|
|
679
|
+
}
|
|
904
680
|
|
|
905
|
-
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
const queryMut = m.queryParams.length > 0 ? "mut " : ""
|
|
912
|
-
l.push(`\tlet ${queryMut}query: Vec<(String, String)> = Vec::new();`)
|
|
913
|
-
emitQueryParamLoop(m.queryParams, l)
|
|
914
|
-
|
|
915
|
-
let bodyArg: string
|
|
916
|
-
if (bi.kind === "json") {
|
|
917
|
-
bodyArg = `crate::runtime_sync::SyncRequestBody::Json(&serde_json::to_value(body).unwrap_or_default())`
|
|
918
|
-
} else if (bi.kind === "form-url") {
|
|
919
|
-
bodyArg = `crate::runtime_sync::SyncRequestBody::FormUrl(body)`
|
|
920
|
-
} else if (bi.kind === "multipart") {
|
|
921
|
-
bodyArg = `crate::runtime_sync::SyncRequestBody::Multipart(body)`
|
|
922
|
-
} else if (bi.kind === "stream") {
|
|
923
|
-
bodyArg = `crate::runtime_sync::SyncRequestBody::Stream(reqwest::blocking::Body::new(body))`
|
|
924
|
-
} else {
|
|
925
|
-
bodyArg = `crate::runtime_sync::SyncRequestBody::None`
|
|
926
|
-
}
|
|
681
|
+
function emitMethod(plan: RustOpPlan, selfAccess: "self" | "self.client", throwOnError: boolean): string[] {
|
|
682
|
+
const l: string[] = []
|
|
683
|
+
const { op } = plan
|
|
684
|
+
const a = access(selfAccess)
|
|
685
|
+
l.push(...docLines(op))
|
|
686
|
+
const sig = signature(plan, false)
|
|
927
687
|
|
|
928
|
-
|
|
929
|
-
const hasInvalidate = m.invalidates.length > 0
|
|
930
|
-
if (hasInvalidate) {
|
|
931
|
-
const pathParamsMapExpr =
|
|
932
|
-
m.pathParams.length > 0
|
|
933
|
-
? `std::collections::HashMap::from([${m.pathParams.map((p) => `(${JSON.stringify(p)}.to_string(), ${rustSnake(p)}.to_string())`).join(", ")}])`
|
|
934
|
-
: `std::collections::HashMap::<String, String>::new()`
|
|
935
|
-
l.push(`\tlet _path_params_map: std::collections::HashMap<String, String> = ${pathParamsMapExpr};`)
|
|
688
|
+
if (op.stream === "realtime") {
|
|
936
689
|
l.push(
|
|
937
|
-
|
|
690
|
+
`pub async fn ${plan.fnName}(${sig.params}) -> Result<crate::realtime::ResumableConnection<serde_json::Value, serde_json::Value>, crate::realtime::RealtimeError> {`,
|
|
938
691
|
)
|
|
939
|
-
l.push(`\tlet
|
|
940
|
-
l.push(
|
|
692
|
+
l.push(`\tlet connect_err = |e: Error| crate::realtime::RealtimeError::Connect(e);`)
|
|
693
|
+
l.push(...pathLines(plan).map((x) => x.replace(/\?;$/, ".map_err(connect_err)?;")))
|
|
694
|
+
l.push(`\tlet url = crate::runtime::build_url(&${a.cfg}.base_url, &url_path, &[]).map_err(connect_err)?;`)
|
|
695
|
+
l.push(`\tlet headers = crate::runtime::auth_headers(&${a.cfg}, &${a.auth}, opts.headers.as_ref()).await;`)
|
|
696
|
+
l.push(`\tlet rt_opts = crate::realtime::ResumableConnectionOpts {`)
|
|
697
|
+
l.push(`\t\treconnect_token: opts.reconnect_token.clone(),`)
|
|
698
|
+
l.push(`\t\tprotocols: opts.protocols.clone(),`)
|
|
699
|
+
l.push(`\t\tmax_reconnect_attempts: opts.max_reconnect_attempts,`)
|
|
700
|
+
l.push(`\t\treconnect_delay_ms: opts.reconnect_delay_ms,`)
|
|
701
|
+
l.push(`\t\tlast_event_id: opts.last_event_id.clone(),`)
|
|
702
|
+
l.push(`\t\theaders,`)
|
|
703
|
+
l.push(`\t\thttp_client: Some(${a.http}.clone()),`)
|
|
704
|
+
l.push(`\t};`)
|
|
941
705
|
l.push(
|
|
942
|
-
`\
|
|
706
|
+
`\tcrate::realtime::ResumableConnection::<serde_json::Value, serde_json::Value>::connect_with_defaults(url.to_string(), rt_opts).await`,
|
|
943
707
|
)
|
|
944
|
-
l.push(
|
|
945
|
-
l.push(
|
|
708
|
+
l.push(`}`)
|
|
709
|
+
l.push(``)
|
|
710
|
+
return l
|
|
946
711
|
}
|
|
947
712
|
|
|
948
|
-
|
|
949
|
-
const emitIdem = m.isIdempotent && !m.isSSE && !m.isWS && !m.isRealtime
|
|
950
|
-
if (emitIdem) {
|
|
713
|
+
if (op.stream === "sse") {
|
|
951
714
|
l.push(
|
|
952
|
-
|
|
715
|
+
`pub fn ${plan.fnName}${sig.generics}(${sig.params}) -> impl Stream<Item = Result<SseEvent, Error>>${sig.where} {`,
|
|
953
716
|
)
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
l.push(`\
|
|
980
|
-
l.push(`\t\
|
|
981
|
-
l.push(`\t\t\
|
|
982
|
-
l.push(`\t\t\
|
|
983
|
-
l.push(`\t\t
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
717
|
+
/* clone everything the stream needs into owned locals: the stream outlives &self/&opts */
|
|
718
|
+
if (plan.pathParams.length === 0) {
|
|
719
|
+
l.push(`\tlet url_path: Result<String, Error> = Ok(${rustString(op.path)}.to_string());`)
|
|
720
|
+
} else {
|
|
721
|
+
const params = plan.pathParams.map((p) => `(${rustString(p.wire)}, ${p.local})`).join(", ")
|
|
722
|
+
l.push(`\tlet url_path = crate::runtime::expand_path(${rustString(op.path)}, &[${params}]);`)
|
|
723
|
+
}
|
|
724
|
+
l.push(...queryLines(plan))
|
|
725
|
+
l.push(
|
|
726
|
+
...headerLines(plan, [
|
|
727
|
+
`\tcrate::runtime::set_header(&mut call_headers, "Accept", "text/event-stream".to_string());`,
|
|
728
|
+
`\tif let Some(ref lei) = opts.last_event_id {`,
|
|
729
|
+
`\t\tcrate::runtime::set_header(&mut call_headers, "Last-Event-ID", lei.clone());`,
|
|
730
|
+
`\t}`,
|
|
731
|
+
]),
|
|
732
|
+
)
|
|
733
|
+
const body = op.body && plan.bodyParam?.local ? op.body : undefined
|
|
734
|
+
if (body?.kind === "json") l.push(`\tlet body_value = serde_json::to_value(${plan.bodyParam?.local});`)
|
|
735
|
+
l.push(`\tlet cfg = ${a.cfg}.clone();`)
|
|
736
|
+
l.push(`\tlet client = ${a.http}.clone();`)
|
|
737
|
+
l.push(`\tlet auth = std::sync::Arc::clone(&${a.auth});`)
|
|
738
|
+
if (body?.kind === "form") l.push(`\tlet form = crate::runtime::form_pairs(${plan.bodyParam?.local});`)
|
|
739
|
+
if (body?.kind === "text") l.push(`\tlet text = bytes::Bytes::from(${plan.bodyParam?.local}.to_string());`)
|
|
740
|
+
if (body?.kind === "multipart" || body?.kind === "binary") l.push(`\tlet raw_body = ${plan.bodyParam?.local};`)
|
|
741
|
+
l.push(`\tasync_stream::stream! {`)
|
|
742
|
+
l.push(`\t\tuse futures_util::StreamExt;`)
|
|
743
|
+
l.push(`\t\tlet url_path = match url_path {`)
|
|
744
|
+
l.push(`\t\t\tOk(p) => p,`)
|
|
745
|
+
l.push(`\t\t\tErr(e) => { yield Err(e); return; }`)
|
|
746
|
+
l.push(`\t\t};`)
|
|
747
|
+
let expr = "crate::runtime::RequestBody::None"
|
|
748
|
+
if (body?.kind === "json") {
|
|
749
|
+
l.push(`\t\tlet body_value = match body_value {`)
|
|
750
|
+
l.push(`\t\t\tOk(v) => v,`)
|
|
751
|
+
l.push(`\t\t\tErr(e) => { yield Err(Error::Json(e)); return; }`)
|
|
752
|
+
l.push(`\t\t};`)
|
|
753
|
+
expr = "crate::runtime::RequestBody::Json(&body_value)"
|
|
754
|
+
} else if (body?.kind === "form") expr = "crate::runtime::RequestBody::FormUrl(form)"
|
|
755
|
+
else if (body?.kind === "text") expr = `crate::runtime::RequestBody::Raw(${rustString(body.contentType)}, text)`
|
|
756
|
+
else if (body?.kind === "multipart") expr = "crate::runtime::RequestBody::Multipart(raw_body)"
|
|
757
|
+
else if (body?.kind === "binary") expr = "crate::runtime::RequestBody::Stream(reqwest::Body::wrap_stream(raw_body))"
|
|
988
758
|
l.push(
|
|
989
|
-
`\t\
|
|
759
|
+
`\t\tlet opened = crate::runtime::open_stream(&client, &cfg, &auth, reqwest::Method::${op.method}, &url_path, &query, ${expr}, Some(&call_headers)).await;`,
|
|
990
760
|
)
|
|
761
|
+
l.push(`\t\tmatch opened {`)
|
|
762
|
+
l.push(`\t\t\tErr(e) => { yield Err(e); }`)
|
|
763
|
+
l.push(`\t\t\tOk(r) => {`)
|
|
764
|
+
l.push(`\t\t\t\tlet mut s = std::pin::pin!(crate::sse::parse_sse_stream(r));`)
|
|
765
|
+
l.push(`\t\t\t\twhile let Some(item) = s.next().await { yield item; }`)
|
|
991
766
|
l.push(`\t\t\t}`)
|
|
992
767
|
l.push(`\t\t}`)
|
|
993
768
|
l.push(`\t}`)
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
if (m.responseType === "") {
|
|
997
|
-
l.push(`\tOk(())`)
|
|
998
769
|
l.push(`}`)
|
|
999
770
|
l.push(``)
|
|
1000
771
|
return l
|
|
1001
772
|
}
|
|
1002
773
|
|
|
1003
|
-
if (
|
|
1004
|
-
l.push(
|
|
1005
|
-
l.push(
|
|
1006
|
-
|
|
1007
|
-
l.push(`\
|
|
774
|
+
if (op.stream === "ws") {
|
|
775
|
+
l.push(`pub async fn ${plan.fnName}(${sig.params}) -> Result<TypedWebSocket, Error> {`)
|
|
776
|
+
l.push(...pathLines(plan))
|
|
777
|
+
l.push(...queryLines(plan))
|
|
778
|
+
l.push(`\tif let Some(ref rt) = opts.reconnect_token {`)
|
|
779
|
+
l.push(`\t\tquery.push(("reconnect_token".to_string(), rt.clone()));`)
|
|
780
|
+
l.push(`\t}`)
|
|
781
|
+
l.push(`\tlet url = crate::runtime::build_url(&${a.cfg}.base_url, &url_path, &query)?;`)
|
|
782
|
+
l.push(`\tlet ws_url = crate::runtime::to_ws_url(url.as_str());`)
|
|
783
|
+
l.push(`\tuse tokio_tungstenite::tungstenite::client::IntoClientRequest;`)
|
|
784
|
+
l.push(`\tlet mut req = ws_url.as_str().into_client_request().map_err(|e| Error::Other(e.to_string()))?;`)
|
|
785
|
+
l.push(`\tfor (k, v) in crate::runtime::auth_headers(&${a.cfg}, &${a.auth}, None).await {`)
|
|
786
|
+
l.push(`\t\tif let (Ok(name), Ok(val)) = (`)
|
|
787
|
+
l.push(`\t\t\ttokio_tungstenite::tungstenite::http::HeaderName::from_bytes(k.as_bytes()),`)
|
|
788
|
+
l.push(`\t\t\tv.parse::<tokio_tungstenite::tungstenite::http::HeaderValue>(),`)
|
|
789
|
+
l.push(`\t\t) {`)
|
|
790
|
+
l.push(`\t\t\treq.headers_mut().insert(name, val);`)
|
|
791
|
+
l.push(`\t\t}`)
|
|
792
|
+
l.push(`\t}`)
|
|
793
|
+
l.push(`\tif let Some(ref protos) = opts.protocols {`)
|
|
794
|
+
l.push(`\t\tif !protos.is_empty() {`)
|
|
795
|
+
l.push(`\t\t\treq.headers_mut().insert(`)
|
|
796
|
+
l.push(`\t\t\t\t"Sec-WebSocket-Protocol",`)
|
|
1008
797
|
l.push(
|
|
1009
|
-
`\
|
|
798
|
+
`\t\t\t\tprotos.join(", ").parse().map_err(|e: tokio_tungstenite::tungstenite::http::header::InvalidHeaderValue| Error::Other(e.to_string()))?,`,
|
|
1010
799
|
)
|
|
800
|
+
l.push(`\t\t\t);`)
|
|
801
|
+
l.push(`\t\t}`)
|
|
802
|
+
l.push(`\t}`)
|
|
803
|
+
l.push(`\tlet (stream, _) = tokio_tungstenite::connect_async(req).await.map_err(|e| Error::Other(e.to_string()))?;`)
|
|
804
|
+
l.push(`\tOk(TypedWebSocket::new(stream))`)
|
|
805
|
+
l.push(`}`)
|
|
806
|
+
l.push(``)
|
|
807
|
+
return l
|
|
1011
808
|
}
|
|
1012
809
|
|
|
810
|
+
l.push(`pub async fn ${plan.fnName}${sig.generics}(${sig.params}) -> ${returnType(plan, throwOnError)}${sig.where} {`)
|
|
811
|
+
l.push(...pathLines(plan))
|
|
812
|
+
l.push(...queryLines(plan))
|
|
813
|
+
l.push(...headerLines(plan, idempotencyLines(plan)))
|
|
814
|
+
const body = bodyExpr(plan, false)
|
|
815
|
+
l.push(...body.lines)
|
|
816
|
+
const call = [
|
|
817
|
+
`crate::runtime::do_request(`,
|
|
818
|
+
`\t&${a.http},`,
|
|
819
|
+
`\t&${a.cfg},`,
|
|
820
|
+
`\t&${a.auth},`,
|
|
821
|
+
`\treqwest::Method::${op.method},`,
|
|
822
|
+
`\t&url_path,`,
|
|
823
|
+
`\t&query,`,
|
|
824
|
+
`\t${body.expr},`,
|
|
825
|
+
`\tSome(&call_headers),`,
|
|
826
|
+
`\topts.timeout,`,
|
|
827
|
+
`\topts.cancel_token.clone(),`,
|
|
828
|
+
`\t${rustString(`${op.method} ${op.path}`)},`,
|
|
829
|
+
`\trequest_meta.as_ref(),`,
|
|
830
|
+
`).await`,
|
|
831
|
+
]
|
|
832
|
+
l.push(...resultLines(plan, throwOnError, false, a, call))
|
|
1013
833
|
l.push(`}`)
|
|
1014
834
|
l.push(``)
|
|
1015
835
|
return l
|
|
1016
836
|
}
|
|
1017
837
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
function rustQueryValueToString(schema: Record<string, unknown>, valuePlace: string): string {
|
|
1028
|
-
if (Array.isArray(schema.enum)) {
|
|
1029
|
-
return `serde_json::to_string(&${valuePlace}).map(|s| s.trim_matches('"').to_string()).unwrap_or_default()`
|
|
1030
|
-
}
|
|
1031
|
-
const t = schema.type
|
|
1032
|
-
if (t === "string") return `${valuePlace}.clone()`
|
|
1033
|
-
if (t === "integer" || t === "number" || t === "boolean") return `${valuePlace}.to_string()`
|
|
1034
|
-
return `serde_json::to_string(&${valuePlace}).unwrap_or_default()`
|
|
838
|
+
function idempotencyLines(plan: RustOpPlan): string[] {
|
|
839
|
+
if (!plan.op.idempotent || plan.op.stream !== null) return []
|
|
840
|
+
/* explicit header (any casing) > opts.idempotency_key > fresh UUID */
|
|
841
|
+
return [
|
|
842
|
+
`\tif !call_headers.keys().any(|k| k.eq_ignore_ascii_case("Idempotency-Key")) {`,
|
|
843
|
+
`\t\tlet key = opts.idempotency_key.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string());`,
|
|
844
|
+
`\t\tcall_headers.insert("Idempotency-Key".to_string(), key);`,
|
|
845
|
+
`\t}`,
|
|
846
|
+
]
|
|
1035
847
|
}
|
|
1036
848
|
|
|
1037
|
-
/**
|
|
1038
|
-
function
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
849
|
+
/** emitSyncMethod is the blocking twin of emitMethod — no async, calls do_request_blocking. */
|
|
850
|
+
function emitSyncMethod(plan: RustOpPlan, selfAccess: "self" | "self.client", throwOnError: boolean): string[] {
|
|
851
|
+
if (!isSyncCapable(plan.op)) return []
|
|
852
|
+
const l: string[] = []
|
|
853
|
+
const { op } = plan
|
|
854
|
+
const a = access(selfAccess)
|
|
855
|
+
l.push(...docLines(op))
|
|
856
|
+
const sig = signature(plan, true)
|
|
857
|
+
l.push(`pub fn ${plan.fnName}${sig.generics}(${sig.params}) -> ${returnType(plan, throwOnError)} {`)
|
|
858
|
+
l.push(...pathLines(plan))
|
|
859
|
+
l.push(...queryLines(plan))
|
|
860
|
+
l.push(...headerLines(plan, idempotencyLines(plan)))
|
|
861
|
+
const body = bodyExpr(plan, true)
|
|
862
|
+
l.push(...body.lines)
|
|
863
|
+
const call = [
|
|
864
|
+
`crate::runtime_sync::do_request_blocking(`,
|
|
865
|
+
`\t&${a.http},`,
|
|
866
|
+
`\t&${a.cfg},`,
|
|
867
|
+
`\t&${a.auth},`,
|
|
868
|
+
`\treqwest::Method::${op.method},`,
|
|
869
|
+
`\t&url_path,`,
|
|
870
|
+
`\t&query,`,
|
|
871
|
+
`\t${body.expr},`,
|
|
872
|
+
`\tSome(&call_headers),`,
|
|
873
|
+
`\topts.timeout,`,
|
|
874
|
+
`\topts.sync_cancel_token.clone(),`,
|
|
875
|
+
`\t${rustString(`${op.method} ${op.path}`)},`,
|
|
876
|
+
`\trequest_meta.as_ref(),`,
|
|
877
|
+
`)`,
|
|
878
|
+
]
|
|
879
|
+
l.push(...resultLines(plan, throwOnError, true, a, call))
|
|
880
|
+
l.push(`}`)
|
|
881
|
+
l.push(``)
|
|
882
|
+
return l
|
|
1051
883
|
}
|
|
1052
884
|
|
|
1053
|
-
function
|
|
1054
|
-
|
|
1055
|
-
if (pathParams.length === 0) return `${JSON.stringify(path)}.to_string()`
|
|
1056
|
-
let fmtStr = path
|
|
1057
|
-
const args: string[] = []
|
|
1058
|
-
for (const p of pathParams) {
|
|
1059
|
-
fmtStr = fmtStr.replace(`{${p}}`, `{}`)
|
|
1060
|
-
args.push(`urlencoding::encode(${rustSnake(p)}).into_owned()`)
|
|
1061
|
-
}
|
|
1062
|
-
return `format!(${JSON.stringify(fmtStr)}, ${args.join(", ")})`
|
|
885
|
+
function isSyncCapable(op: SdkOp): boolean {
|
|
886
|
+
return op.stream === null
|
|
1063
887
|
}
|
|
1064
888
|
|
|
1065
|
-
/*
|
|
1066
|
-
const RUST_RUNTIME_TYPES = new Set([
|
|
1067
|
-
"ClientConfig",
|
|
1068
|
-
"Error",
|
|
1069
|
-
"InvalidationConfig",
|
|
1070
|
-
"LogEntry",
|
|
1071
|
-
"OnAuthExpiredHook",
|
|
1072
|
-
"OnLogHook",
|
|
1073
|
-
"OnRequestHook",
|
|
1074
|
-
"OnResponseHook",
|
|
1075
|
-
"RequestBody",
|
|
1076
|
-
"RequestContext",
|
|
1077
|
-
"RequestMeta",
|
|
1078
|
-
"ResponseContext",
|
|
1079
|
-
"ResponseMeta",
|
|
1080
|
-
"SdkResult",
|
|
1081
|
-
"SseEvent",
|
|
1082
|
-
"StaleEntry",
|
|
1083
|
-
"StaleTracker",
|
|
1084
|
-
"StaleTrackerSync",
|
|
1085
|
-
"StatusError",
|
|
1086
|
-
"SyncClientConfig",
|
|
1087
|
-
"SyncRequestBody",
|
|
1088
|
-
"SyncRequestContext",
|
|
1089
|
-
"SyncResponseContext",
|
|
1090
|
-
"TypedWebSocket",
|
|
1091
|
-
])
|
|
889
|
+
/* ── resource tree ── */
|
|
1092
890
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
l.push(``)
|
|
891
|
+
type RustNode = {
|
|
892
|
+
path: string[]
|
|
893
|
+
modName: string
|
|
894
|
+
structName: string
|
|
895
|
+
syncStructName: string
|
|
896
|
+
ns: IRNamespace
|
|
897
|
+
children: Array<{ accessor: string; node: RustNode }>
|
|
898
|
+
plans: RustOpPlan[]
|
|
899
|
+
}
|
|
1103
900
|
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
901
|
+
function hasSync(node: RustNode): boolean {
|
|
902
|
+
return node.plans.some((p) => isSyncCapable(p.op)) || node.children.some((c) => hasSync(c.node))
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function buildTree(model: SdkModel, names: RustNames, throwOnError: boolean): { root: RustNode; plans: RustOpPlan[] } {
|
|
906
|
+
const plans: RustOpPlan[] = []
|
|
907
|
+
function visit(ns: IRNamespace, path: string[], modName: string, reserved: string[]): RustNode {
|
|
908
|
+
const isRoot = path.length === 0
|
|
909
|
+
const pascal = path.map(rustTypeIdent).join("")
|
|
910
|
+
const node: RustNode = {
|
|
911
|
+
children: [],
|
|
912
|
+
modName,
|
|
913
|
+
ns,
|
|
914
|
+
path,
|
|
915
|
+
plans: [],
|
|
916
|
+
structName: isRoot ? "Client" : names.scope.claim(`${pascal}Resource`),
|
|
917
|
+
syncStructName: isRoot ? "SyncClient" : names.scope.claim(`${pascal}ResourceSync`),
|
|
918
|
+
}
|
|
919
|
+
const members = new NameScope(reserved)
|
|
920
|
+
const modules = new NameScope()
|
|
921
|
+
const childInfo: Array<{ accessor: string; seg: string; ns: IRNamespace; mod: string }> = []
|
|
922
|
+
for (const [seg, childNs] of namespacesOf(ns)) {
|
|
923
|
+
childInfo.push({
|
|
924
|
+
accessor: members.claim(rustPlainSnake(seg)),
|
|
925
|
+
mod: modules.claim(rustPlainSnake(seg)),
|
|
926
|
+
ns: childNs,
|
|
927
|
+
seg,
|
|
928
|
+
})
|
|
929
|
+
}
|
|
930
|
+
for (const [seg, irOp] of methodsOf(ns)) {
|
|
931
|
+
const op = model.opsById.get(irOp.id)
|
|
932
|
+
if (!op) continue
|
|
933
|
+
const ident = rustValueIdent(seg)
|
|
934
|
+
const plain = ident.startsWith("r#") ? ident.slice(2) : ident
|
|
935
|
+
const claimed = members.claim(plain)
|
|
936
|
+
const plan = planOp(op, claimed === plain ? ident : claimed, [...path, seg], names, throwOnError, model)
|
|
937
|
+
node.plans.push(plan)
|
|
938
|
+
plans.push(plan)
|
|
939
|
+
}
|
|
940
|
+
for (const c of childInfo) {
|
|
941
|
+
node.children.push({ accessor: c.accessor, node: visit(c.ns, [...path, c.seg], c.mod, ["client", "new"]) })
|
|
942
|
+
}
|
|
943
|
+
return node
|
|
944
|
+
}
|
|
945
|
+
const root = visit(model.ir.tree, [], "", ["inner", "new", "is_stale"])
|
|
946
|
+
return { plans, root }
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/* ── files ── */
|
|
1109
950
|
|
|
951
|
+
function buildRustTypes(model: SdkModel, names: RustNames): string {
|
|
952
|
+
const body: string[] = []
|
|
1110
953
|
/* always emit shared error-envelope field type used by per-op error structs */
|
|
1111
954
|
body.push(`#[derive(Debug, Clone, Serialize, Deserialize)]`)
|
|
1112
955
|
body.push(`pub struct ErrField {`)
|
|
@@ -1115,20 +958,30 @@ function buildRustTypes(spec: OpenApiSpec): string {
|
|
|
1115
958
|
body.push(`\tpub path: String,`)
|
|
1116
959
|
body.push(`}`)
|
|
1117
960
|
body.push(``)
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
body.push(rustType)
|
|
961
|
+
for (const name of model.schemaNames) {
|
|
962
|
+
if (Object.hasOwn(model.ir.schemaDescriptions, name)) body.push(...rustDoc(model.ir.schemaDescriptions[name]))
|
|
963
|
+
body.push(irRenderTopLevelRust(names.types.ref(name), model.ir.schemas[name], names.decls, undefined, names.types))
|
|
1122
964
|
body.push(``)
|
|
1123
965
|
}
|
|
966
|
+
return body.join("\n")
|
|
967
|
+
}
|
|
1124
968
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
969
|
+
function typesFile(schemaSource: string, names: RustNames): string {
|
|
970
|
+
const l: string[] = []
|
|
971
|
+
l.push(`/* Code generated by honey. DO NOT EDIT. */`)
|
|
972
|
+
l.push(``)
|
|
973
|
+
l.push(`#![allow(unused_imports, clippy::large_enum_variant)]`)
|
|
974
|
+
l.push(`use serde::{Serialize, Deserialize};`)
|
|
975
|
+
l.push(`use serde_repr::{Serialize_repr, Deserialize_repr};`)
|
|
976
|
+
l.push(`use std::collections::HashMap;`)
|
|
977
|
+
l.push(``)
|
|
978
|
+
l.push(schemaSource)
|
|
979
|
+
for (const name of [...names.decls.keys()].sort(cmpCodeUnit)) {
|
|
980
|
+
const decl = names.decls.get(name)
|
|
981
|
+
if (!decl) continue
|
|
982
|
+
l.push(decl)
|
|
983
|
+
l.push(``)
|
|
1129
984
|
}
|
|
1130
|
-
|
|
1131
|
-
l.push(body.join("\n"))
|
|
1132
985
|
return l.join("\n")
|
|
1133
986
|
}
|
|
1134
987
|
|
|
@@ -1136,6 +989,7 @@ function buildCargoToml(
|
|
|
1136
989
|
crateName: string,
|
|
1137
990
|
meta: { version: string; description?: string; homepage?: string; repository?: string; license?: string },
|
|
1138
991
|
): string {
|
|
992
|
+
/* TOML basic strings share JSON's escapes */
|
|
1139
993
|
const pkgLines = [
|
|
1140
994
|
`[package]`,
|
|
1141
995
|
`name = "${crateName}"`,
|
|
@@ -1180,6 +1034,7 @@ function buildLibRs(hasRealtime: boolean): string {
|
|
|
1180
1034
|
`pub mod errors;`,
|
|
1181
1035
|
`pub mod invalidation;`,
|
|
1182
1036
|
`pub mod invalidation_sync;`,
|
|
1037
|
+
`pub mod nullable;`,
|
|
1183
1038
|
`pub mod resources;`,
|
|
1184
1039
|
`pub mod result;`,
|
|
1185
1040
|
`pub mod runtime;`,
|
|
@@ -1188,9 +1043,7 @@ function buildLibRs(hasRealtime: boolean): string {
|
|
|
1188
1043
|
`pub mod types;`,
|
|
1189
1044
|
`pub mod ws;`,
|
|
1190
1045
|
]
|
|
1191
|
-
if (hasRealtime)
|
|
1192
|
-
lines.push(`pub mod realtime;`)
|
|
1193
|
-
}
|
|
1046
|
+
if (hasRealtime) lines.push(`pub mod realtime;`)
|
|
1194
1047
|
lines.push(``)
|
|
1195
1048
|
lines.push(`pub use client::Client;`)
|
|
1196
1049
|
lines.push(`pub use client::SyncClient;`)
|
|
@@ -1202,21 +1055,51 @@ function buildLibRs(hasRealtime: boolean): string {
|
|
|
1202
1055
|
return lines.join("\n")
|
|
1203
1056
|
}
|
|
1204
1057
|
|
|
1205
|
-
function
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1058
|
+
function fileHeader(sync: boolean): string[] {
|
|
1059
|
+
const lines: string[] = []
|
|
1060
|
+
lines.push(`/* Code generated by honey. DO NOT EDIT. */`)
|
|
1061
|
+
lines.push(``)
|
|
1062
|
+
lines.push(`#![allow(unused_imports, unused_variables, unused_mut, dead_code, deprecated)]`)
|
|
1063
|
+
lines.push(`use std::sync::Arc;`)
|
|
1064
|
+
lines.push(`use futures_util::{self, Stream};`)
|
|
1065
|
+
lines.push(`use reqwest;`)
|
|
1066
|
+
lines.push(`use serde::{Serialize, Deserialize};`)
|
|
1067
|
+
lines.push(`use tokio_tungstenite;`)
|
|
1068
|
+
lines.push(`use crate::client::ClientInner;`)
|
|
1069
|
+
if (sync) lines.push(`use crate::client::SyncClientInner;`)
|
|
1070
|
+
lines.push(`use crate::errors::Error;`)
|
|
1071
|
+
lines.push(`use crate::result::SdkResult;`)
|
|
1072
|
+
lines.push(`use crate::types::*;`)
|
|
1073
|
+
lines.push(`use crate::sse::SseEvent;`)
|
|
1074
|
+
lines.push(`use crate::ws::TypedWebSocket;`)
|
|
1075
|
+
lines.push(``)
|
|
1076
|
+
return lines
|
|
1077
|
+
}
|
|
1209
1078
|
|
|
1210
|
-
|
|
1079
|
+
function staleInit(tracker: string): string[] {
|
|
1080
|
+
return [
|
|
1081
|
+
`\t\tlet stale = match cfg.invalidation.clone() {`,
|
|
1082
|
+
`\t\t\tSome(mut inv) if inv.stale_time > 0 => {`,
|
|
1083
|
+
`\t\t\t\tif inv.stale_max_entries == 0 {`,
|
|
1084
|
+
`\t\t\t\t\tinv.stale_max_entries = cfg.stale_max_entries;`,
|
|
1085
|
+
`\t\t\t\t}`,
|
|
1086
|
+
`\t\t\t\tSome(${tracker}::new(Some(&inv)))`,
|
|
1087
|
+
`\t\t\t}`,
|
|
1088
|
+
`\t\t\t_ => None,`,
|
|
1089
|
+
`\t\t};`,
|
|
1090
|
+
]
|
|
1091
|
+
}
|
|
1211
1092
|
|
|
1093
|
+
function buildRustClient(
|
|
1094
|
+
root: RustNode,
|
|
1095
|
+
plans: RustOpPlan[],
|
|
1096
|
+
throwOnError: boolean,
|
|
1097
|
+
auth: { headerName: string; prefix: string },
|
|
1098
|
+
serviceMap: Record<string, Record<string, unknown>>,
|
|
1099
|
+
realtimeIds: Set<string>,
|
|
1100
|
+
): string {
|
|
1212
1101
|
const body: string[] = []
|
|
1213
1102
|
|
|
1214
|
-
/* build a realtime lookup map from collected methods */
|
|
1215
|
-
const realtimeLookup = new Map<string, boolean>()
|
|
1216
|
-
for (const m of methods) {
|
|
1217
|
-
realtimeLookup.set(`${m.resource}.${m.action}`, m.isRealtime)
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
1103
|
body.push(`pub(crate) struct ServiceEntry {`)
|
|
1221
1104
|
body.push(`\tpub method: &'static str,`)
|
|
1222
1105
|
body.push(`\tpub path: &'static str,`)
|
|
@@ -1227,28 +1110,25 @@ function buildRustClient(spec: OpenApiSpec, options: RustSDKOptions, ir: ReturnT
|
|
|
1227
1110
|
body.push(`\tpub invalidate: &'static [&'static str],`)
|
|
1228
1111
|
body.push(`}`)
|
|
1229
1112
|
body.push(``)
|
|
1230
|
-
|
|
1231
1113
|
body.push(
|
|
1232
1114
|
`pub(crate) static INVALIDATION_MAP: once_cell::sync::Lazy<std::collections::HashMap<&'static str, std::collections::HashMap<&'static str, ServiceEntry>>> =`,
|
|
1233
1115
|
)
|
|
1234
1116
|
body.push(`\tonce_cell::sync::Lazy::new(|| {`)
|
|
1235
1117
|
body.push(`\t\tlet mut map = std::collections::HashMap::new();`)
|
|
1236
|
-
for (const [resource, actions] of Object.entries(serviceMap).sort(([a], [b]) => a
|
|
1118
|
+
for (const [resource, actions] of Object.entries(serviceMap).sort(([a], [b]) => cmpCodeUnit(a, b))) {
|
|
1237
1119
|
body.push(`\t\t{`)
|
|
1238
1120
|
body.push(`\t\t\tlet mut resource_map = std::collections::HashMap::new();`)
|
|
1239
1121
|
for (const [action, entry] of Object.entries(actions as Record<string, Record<string, unknown>>).sort(([a], [b]) =>
|
|
1240
|
-
a
|
|
1122
|
+
cmpCodeUnit(a, b),
|
|
1241
1123
|
)) {
|
|
1242
|
-
const inv = entry.invalidate as string[] | undefined
|
|
1243
|
-
const
|
|
1244
|
-
const
|
|
1245
|
-
const paramsStr = params && params.length > 0 ? `&[${params.map((s) => JSON.stringify(s)).join(", ")}]` : `&[]`
|
|
1246
|
-
const isRealtime = realtimeLookup.get(`${resource}.${action}`) ?? false
|
|
1124
|
+
const inv = (entry.invalidate as string[] | undefined) ?? []
|
|
1125
|
+
const params = (entry.params as string[] | undefined) ?? []
|
|
1126
|
+
const opId = action === "_call" ? resource : `${resource}.${action}`
|
|
1247
1127
|
body.push(
|
|
1248
|
-
`\t\t\tresource_map.insert(${
|
|
1128
|
+
`\t\t\tresource_map.insert(${rustString(action)}, ServiceEntry { method: ${rustString(String(entry.method))}, path: ${rustString(String(entry.path))}, params: &[${params.map(rustString).join(", ")}], sse: ${entry.sse === true}, ws: ${entry.ws === true}, realtime: ${realtimeIds.has(opId)}, invalidate: &[${inv.map(rustString).join(", ")}] });`,
|
|
1249
1129
|
)
|
|
1250
1130
|
}
|
|
1251
|
-
body.push(`\t\t\tmap.insert(${
|
|
1131
|
+
body.push(`\t\t\tmap.insert(${rustString(resource)}, resource_map);`)
|
|
1252
1132
|
body.push(`\t\t}`)
|
|
1253
1133
|
}
|
|
1254
1134
|
body.push(`\t\tmap`)
|
|
@@ -1259,72 +1139,52 @@ function buildRustClient(spec: OpenApiSpec, options: RustSDKOptions, ir: ReturnT
|
|
|
1259
1139
|
body.push(`\tpub(crate) cfg: crate::runtime::ClientConfig,`)
|
|
1260
1140
|
body.push(`\tpub(crate) stale: Option<crate::invalidation::StaleTracker>,`)
|
|
1261
1141
|
body.push(`\tpub(crate) http_client: reqwest::Client,`)
|
|
1142
|
+
body.push(`\tpub(crate) auth: std::sync::Arc<crate::runtime::AuthState>,`)
|
|
1262
1143
|
body.push(`}`)
|
|
1263
1144
|
body.push(``)
|
|
1264
1145
|
|
|
1265
|
-
const auth = detectAuthScheme(spec as Record<string, unknown>)
|
|
1266
1146
|
body.push(`/// Client is the generated SDK client.`)
|
|
1267
1147
|
body.push(`pub struct Client {`)
|
|
1268
1148
|
body.push(`\tpub(crate) inner: std::sync::Arc<ClientInner>,`)
|
|
1269
|
-
for (const
|
|
1270
|
-
const safeName = safeResourceName(seg)
|
|
1271
|
-
body.push(`\t${rustSnake(safeName)}: ${rustPascal(safeName)}Resource,`)
|
|
1272
|
-
}
|
|
1149
|
+
for (const c of root.children) body.push(`\t${c.accessor}: ${c.node.structName},`)
|
|
1273
1150
|
body.push(`}`)
|
|
1274
1151
|
body.push(``)
|
|
1275
|
-
|
|
1276
1152
|
body.push(`impl Client {`)
|
|
1277
1153
|
body.push(`\t/// new creates a new SDK client. base_url must be non-empty.`)
|
|
1278
1154
|
body.push(`\tpub fn new(mut cfg: crate::runtime::ClientConfig) -> Self {`)
|
|
1279
1155
|
body.push(`\t\tif cfg.base_url.is_empty() {`)
|
|
1280
1156
|
body.push(`\t\t\tpanic!("honey sdk: ClientConfig.base_url must not be empty");`)
|
|
1281
1157
|
body.push(`\t\t}`)
|
|
1282
|
-
body.push(`\t\t/* http_client: None →
|
|
1283
|
-
body.push(`\t\tlet http_client =
|
|
1284
|
-
body.push(`\t\t\
|
|
1285
|
-
body.push(`\t\t
|
|
1286
|
-
body.push(`\t\t\tcfg.http_client.take().unwrap()`)
|
|
1158
|
+
body.push(`\t\t/* http_client: None → a default client that never redirects to another origin */`)
|
|
1159
|
+
body.push(`\t\tlet http_client = match cfg.http_client.take() {`)
|
|
1160
|
+
body.push(`\t\t\tSome(c) => c,`)
|
|
1161
|
+
body.push(`\t\t\tNone => crate::runtime::default_http_client(),`)
|
|
1287
1162
|
body.push(`\t\t};`)
|
|
1288
1163
|
body.push(`\t\tif cfg.auth_header_name.is_none() {`)
|
|
1289
|
-
body.push(`\t\t\tcfg.auth_header_name = Some(${
|
|
1164
|
+
body.push(`\t\t\tcfg.auth_header_name = Some(${rustString(auth.headerName)}.to_string());`)
|
|
1290
1165
|
body.push(`\t\t}`)
|
|
1291
1166
|
body.push(`\t\tif cfg.auth_header_prefix.is_none() {`)
|
|
1292
|
-
body.push(`\t\t\tcfg.auth_header_prefix = Some(${
|
|
1167
|
+
body.push(`\t\t\tcfg.auth_header_prefix = Some(${rustString(auth.prefix)}.to_string());`)
|
|
1293
1168
|
body.push(`\t\t}`)
|
|
1294
|
-
body.push(
|
|
1295
|
-
body.push(`\t\
|
|
1296
|
-
body.push(`\t\
|
|
1297
|
-
body.push(`\t\t\t} else { None }`)
|
|
1298
|
-
body.push(`\t\t} else { None };`)
|
|
1299
|
-
body.push(`\t\tlet inner = std::sync::Arc::new(ClientInner { cfg, stale, http_client });`)
|
|
1169
|
+
body.push(...staleInit("crate::invalidation::StaleTracker"))
|
|
1170
|
+
body.push(`\t\tlet auth = std::sync::Arc::new(crate::runtime::AuthState::new(cfg.bearer_token.clone()));`)
|
|
1171
|
+
body.push(`\t\tlet inner = std::sync::Arc::new(ClientInner { cfg, stale, http_client, auth });`)
|
|
1300
1172
|
body.push(`\t\tClient {`)
|
|
1301
|
-
for (const
|
|
1302
|
-
|
|
1303
|
-
body.push(
|
|
1304
|
-
`\t\t\t${rustSnake(safeName)}: crate::resources::${rustSnake(safeName)}::new_${rustSnake(safeName)}_resource(&inner),`,
|
|
1305
|
-
)
|
|
1173
|
+
for (const c of root.children) {
|
|
1174
|
+
body.push(`\t\t\t${c.accessor}: crate::resources::${c.node.modName}::new_resource(&inner),`)
|
|
1306
1175
|
}
|
|
1307
1176
|
body.push(`\t\t\tinner,`)
|
|
1308
1177
|
body.push(`\t\t}`)
|
|
1309
1178
|
body.push(`\t}`)
|
|
1310
1179
|
body.push(``)
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
body.push(`\t/// ${rustSnake(safeName)} returns a reference to the ${structName}.`)
|
|
1316
|
-
body.push(`\tpub fn ${rustSnake(safeName)}(&self) -> &${structName} {`)
|
|
1317
|
-
body.push(`\t\t&self.${rustSnake(safeName)}`)
|
|
1180
|
+
for (const c of root.children) {
|
|
1181
|
+
body.push(`\t/// ${c.accessor} returns a reference to the ${c.node.structName}.`)
|
|
1182
|
+
body.push(`\tpub fn ${c.accessor}(&self) -> &${c.node.structName} {`)
|
|
1183
|
+
body.push(`\t\t&self.${c.accessor}`)
|
|
1318
1184
|
body.push(`\t}`)
|
|
1319
1185
|
body.push(``)
|
|
1320
1186
|
}
|
|
1321
|
-
|
|
1322
|
-
for (const m of topLevel.sort((a, b) => a.resource.localeCompare(b.resource))) {
|
|
1323
|
-
body.push(...emitMethod(m, "self", throwOnError))
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
/* is_stale — public predicate consumers call to drive refetch decisions.
|
|
1327
|
-
* Delegates to the StaleTracker; returns false when invalidation is disabled. */
|
|
1187
|
+
for (const plan of root.plans) body.push(...emitMethod(plan, "self", throwOnError).map((x) => `\t${x}`))
|
|
1328
1188
|
body.push(`\t/// is_stale reports whether (method, path) is currently within an active stale window.`)
|
|
1329
1189
|
body.push(`\tpub async fn is_stale(&self, method: &str, path: &str) -> bool {`)
|
|
1330
1190
|
body.push(`\t\tmatch self.inner.stale {`)
|
|
@@ -1332,101 +1192,62 @@ function buildRustClient(spec: OpenApiSpec, options: RustSDKOptions, ir: ReturnT
|
|
|
1332
1192
|
body.push(`\t\t\tNone => false,`)
|
|
1333
1193
|
body.push(`\t\t}`)
|
|
1334
1194
|
body.push(`\t}`)
|
|
1335
|
-
body.push(``)
|
|
1336
|
-
|
|
1337
1195
|
body.push(`}`)
|
|
1338
1196
|
body.push(``)
|
|
1339
1197
|
|
|
1340
|
-
const
|
|
1341
|
-
for (const m of topLevel.sort((a, b) => a.opId.localeCompare(b.opId))) {
|
|
1342
|
-
const optsName = optsStructName(m)
|
|
1343
|
-
if (topLevelOptsEmitted.has(optsName)) continue
|
|
1344
|
-
topLevelOptsEmitted.add(optsName)
|
|
1345
|
-
body.push(...emitOptsStruct(m))
|
|
1346
|
-
}
|
|
1198
|
+
for (const plan of root.plans) body.push(...emitOptsStruct(plan), ...emitErrorTypes(plan))
|
|
1347
1199
|
|
|
1348
1200
|
/* ── SyncClient ── */
|
|
1349
|
-
|
|
1350
|
-
/* root namespaces that have at least one non-SSE/non-WS descendant method */
|
|
1351
|
-
const syncNsSegs = namespacesOf(ir.tree)
|
|
1352
|
-
.map(([seg]) => seg)
|
|
1353
|
-
.filter((seg) => {
|
|
1354
|
-
const nsMs = methods.filter((m) => !m.isSSE && !m.isWS && m.namePath[0] === seg)
|
|
1355
|
-
return nsMs.length > 0
|
|
1356
|
-
})
|
|
1357
|
-
|
|
1201
|
+
const syncChildren = root.children.filter((c) => hasSync(c.node))
|
|
1358
1202
|
body.push(`pub(crate) struct SyncClientInner {`)
|
|
1359
1203
|
body.push(`\tpub(crate) cfg: crate::runtime_sync::SyncClientConfig,`)
|
|
1360
1204
|
body.push(`\tpub(crate) stale: Option<crate::invalidation_sync::StaleTrackerSync>,`)
|
|
1361
1205
|
body.push(`\tpub(crate) http_client: reqwest::blocking::Client,`)
|
|
1206
|
+
body.push(`\tpub(crate) auth: std::sync::Arc<crate::runtime_sync::SyncAuthState>,`)
|
|
1362
1207
|
body.push(`}`)
|
|
1363
1208
|
body.push(``)
|
|
1364
|
-
|
|
1365
1209
|
body.push(`/// SyncClient is the generated blocking SDK client.`)
|
|
1366
1210
|
body.push(`/// WARNING: Do not use from inside a tokio async task — reqwest blocking will deadlock.`)
|
|
1367
1211
|
body.push(`pub struct SyncClient {`)
|
|
1368
1212
|
body.push(`\tpub(crate) inner: std::sync::Arc<SyncClientInner>,`)
|
|
1369
|
-
for (const
|
|
1370
|
-
const safeName = safeResourceName(seg)
|
|
1371
|
-
body.push(`\t${rustSnake(safeName)}: ${rustPascal(safeName)}ResourceSync,`)
|
|
1372
|
-
}
|
|
1213
|
+
for (const c of syncChildren) body.push(`\t${c.accessor}: ${c.node.syncStructName},`)
|
|
1373
1214
|
body.push(`}`)
|
|
1374
1215
|
body.push(``)
|
|
1375
|
-
|
|
1376
1216
|
body.push(`impl SyncClient {`)
|
|
1377
1217
|
body.push(`\t/// new creates a new blocking SDK client. base_url must be non-empty.`)
|
|
1378
1218
|
body.push(`\tpub fn new(mut cfg: crate::runtime_sync::SyncClientConfig) -> Self {`)
|
|
1379
1219
|
body.push(`\t\tif cfg.base_url.is_empty() {`)
|
|
1380
1220
|
body.push(`\t\t\tpanic!("honey sdk: SyncClientConfig.base_url must not be empty");`)
|
|
1381
1221
|
body.push(`\t\t}`)
|
|
1382
|
-
body.push(`\t\tlet http_client =
|
|
1383
|
-
body.push(`\t\t\
|
|
1384
|
-
body.push(`\t\t
|
|
1385
|
-
body.push(`\t\t\tcfg.http_client.take().unwrap()`)
|
|
1222
|
+
body.push(`\t\tlet http_client = match cfg.http_client.take() {`)
|
|
1223
|
+
body.push(`\t\t\tSome(c) => c,`)
|
|
1224
|
+
body.push(`\t\t\tNone => crate::runtime_sync::default_http_client_blocking(),`)
|
|
1386
1225
|
body.push(`\t\t};`)
|
|
1387
1226
|
body.push(`\t\tif cfg.auth_header_name.is_none() {`)
|
|
1388
|
-
body.push(`\t\t\tcfg.auth_header_name = Some(${
|
|
1227
|
+
body.push(`\t\t\tcfg.auth_header_name = Some(${rustString(auth.headerName)}.to_string());`)
|
|
1389
1228
|
body.push(`\t\t}`)
|
|
1390
1229
|
body.push(`\t\tif cfg.auth_header_prefix.is_none() {`)
|
|
1391
|
-
body.push(`\t\t\tcfg.auth_header_prefix = Some(${
|
|
1230
|
+
body.push(`\t\t\tcfg.auth_header_prefix = Some(${rustString(auth.prefix)}.to_string());`)
|
|
1392
1231
|
body.push(`\t\t}`)
|
|
1393
|
-
body.push(
|
|
1394
|
-
body.push(`\t\
|
|
1395
|
-
body.push(`\t\
|
|
1396
|
-
body.push(`\t\t\t} else { None }`)
|
|
1397
|
-
body.push(`\t\t} else { None };`)
|
|
1398
|
-
body.push(`\t\tlet inner = std::sync::Arc::new(SyncClientInner { cfg, stale, http_client });`)
|
|
1232
|
+
body.push(...staleInit("crate::invalidation_sync::StaleTrackerSync"))
|
|
1233
|
+
body.push(`\t\tlet auth = std::sync::Arc::new(crate::runtime_sync::SyncAuthState::new(cfg.bearer_token.clone()));`)
|
|
1234
|
+
body.push(`\t\tlet inner = std::sync::Arc::new(SyncClientInner { cfg, stale, http_client, auth });`)
|
|
1399
1235
|
body.push(`\t\tSyncClient {`)
|
|
1400
|
-
for (const
|
|
1401
|
-
|
|
1402
|
-
body.push(
|
|
1403
|
-
`\t\t\t${rustSnake(safeName)}: ${rustPascal(safeName)}ResourceSync { client: std::sync::Arc::clone(&inner) },`,
|
|
1404
|
-
)
|
|
1236
|
+
for (const c of syncChildren) {
|
|
1237
|
+
body.push(`\t\t\t${c.accessor}: crate::resources::${c.node.modName}::new_resource_sync(&inner),`)
|
|
1405
1238
|
}
|
|
1406
1239
|
body.push(`\t\t\tinner,`)
|
|
1407
1240
|
body.push(`\t\t}`)
|
|
1408
1241
|
body.push(`\t}`)
|
|
1409
1242
|
body.push(``)
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
body.push(`\t/// ${rustSnake(safeName)} returns a reference to the ${structName}.`)
|
|
1415
|
-
body.push(`\tpub fn ${rustSnake(safeName)}(&self) -> &${structName} {`)
|
|
1416
|
-
body.push(`\t\t&self.${rustSnake(safeName)}`)
|
|
1243
|
+
for (const c of syncChildren) {
|
|
1244
|
+
body.push(`\t/// ${c.accessor} returns a reference to the ${c.node.syncStructName}.`)
|
|
1245
|
+
body.push(`\tpub fn ${c.accessor}(&self) -> &${c.node.syncStructName} {`)
|
|
1246
|
+
body.push(`\t\t&self.${c.accessor}`)
|
|
1417
1247
|
body.push(`\t}`)
|
|
1418
1248
|
body.push(``)
|
|
1419
1249
|
}
|
|
1420
|
-
|
|
1421
|
-
/* top-level sync methods (operationIds without resource.action split, e.g. `getUser`)
|
|
1422
|
-
mirror the async Client's top-level methods. SSE/WS skipped — no sync variant. */
|
|
1423
|
-
const syncTopLevel = topLevel.filter((m) => !m.isSSE && !m.isWS)
|
|
1424
|
-
for (const m of syncTopLevel.sort((a, b) => a.resource.localeCompare(b.resource))) {
|
|
1425
|
-
body.push(...emitSyncMethod(m, "self", throwOnError))
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
/* is_stale (sync) — public predicate consumers call to drive refetch decisions.
|
|
1429
|
-
* Delegates to the StaleTrackerSync; returns false when invalidation is disabled. */
|
|
1250
|
+
for (const plan of root.plans) body.push(...emitSyncMethod(plan, "self", throwOnError).map((x) => `\t${x}`))
|
|
1430
1251
|
body.push(`\t/// is_stale reports whether (method, path) is currently within an active stale window.`)
|
|
1431
1252
|
body.push(`\tpub fn is_stale(&self, method: &str, path: &str) -> bool {`)
|
|
1432
1253
|
body.push(`\t\tmatch self.inner.stale {`)
|
|
@@ -1434,22 +1255,18 @@ function buildRustClient(spec: OpenApiSpec, options: RustSDKOptions, ir: ReturnT
|
|
|
1434
1255
|
body.push(`\t\t\tNone => false,`)
|
|
1435
1256
|
body.push(`\t\t}`)
|
|
1436
1257
|
body.push(`\t}`)
|
|
1437
|
-
body.push(``)
|
|
1438
|
-
|
|
1439
1258
|
body.push(`}`)
|
|
1440
1259
|
body.push(``)
|
|
1441
|
-
|
|
1442
|
-
const bodyStr = body.join("\n")
|
|
1260
|
+
void plans
|
|
1443
1261
|
|
|
1444
1262
|
const out: string[] = []
|
|
1445
1263
|
out.push(`/* Code generated by honey. DO NOT EDIT. */`)
|
|
1446
1264
|
out.push(``)
|
|
1447
|
-
out.push(`#![allow(unused_imports, unused_variables, dead_code)]`)
|
|
1265
|
+
out.push(`#![allow(unused_imports, unused_variables, unused_mut, dead_code, deprecated)]`)
|
|
1448
1266
|
out.push(`use std::sync::Arc;`)
|
|
1449
1267
|
out.push(`use once_cell;`)
|
|
1450
1268
|
out.push(`use reqwest;`)
|
|
1451
1269
|
out.push(`use tokio_tungstenite;`)
|
|
1452
|
-
out.push(`use urlencoding;`)
|
|
1453
1270
|
out.push(`use futures_util::{self, Stream};`)
|
|
1454
1271
|
out.push(`use crate::errors::Error;`)
|
|
1455
1272
|
out.push(`use crate::resources::*;`)
|
|
@@ -1459,238 +1276,127 @@ function buildRustClient(spec: OpenApiSpec, options: RustSDKOptions, ir: ReturnT
|
|
|
1459
1276
|
out.push(`use crate::types::*;`)
|
|
1460
1277
|
out.push(`use crate::ws::TypedWebSocket;`)
|
|
1461
1278
|
out.push(``)
|
|
1462
|
-
out.push(
|
|
1279
|
+
out.push(body.join("\n"))
|
|
1463
1280
|
return out.join("\n")
|
|
1464
1281
|
}
|
|
1465
1282
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
/** Emit standard file header imports for a resource .rs file. */
|
|
1469
|
-
function emitRustResourceFileHeader(rMethods: MethodInfo[]): string[] {
|
|
1470
|
-
const hasSSE = rMethods.some((m) => m.isSSE)
|
|
1471
|
-
const syncMethods = rMethods.filter((m) => !m.isSSE && !m.isWS)
|
|
1472
|
-
const hasSyncMethods = syncMethods.length > 0
|
|
1473
|
-
const lines: string[] = []
|
|
1474
|
-
lines.push(`/* Code generated by honey. DO NOT EDIT. */`)
|
|
1475
|
-
lines.push(``)
|
|
1476
|
-
lines.push(`#![allow(unused_imports, unused_variables, dead_code)]`)
|
|
1477
|
-
lines.push(`use std::sync::Arc;`)
|
|
1478
|
-
lines.push(`use futures_util::{self, Stream};`)
|
|
1479
|
-
lines.push(`use reqwest;`)
|
|
1480
|
-
lines.push(`use serde::{Serialize, Deserialize};`)
|
|
1481
|
-
lines.push(`use tokio_tungstenite;`)
|
|
1482
|
-
lines.push(`use urlencoding;`)
|
|
1483
|
-
lines.push(`use crate::client::ClientInner;`)
|
|
1484
|
-
if (hasSyncMethods) lines.push(`use crate::client::SyncClientInner;`)
|
|
1485
|
-
lines.push(`use crate::errors::Error;`)
|
|
1486
|
-
lines.push(`use crate::result::SdkResult;`)
|
|
1487
|
-
lines.push(`use crate::runtime::*;`)
|
|
1488
|
-
if (hasSyncMethods) lines.push(`use crate::runtime_sync::*;`)
|
|
1489
|
-
lines.push(`use crate::types::*;`)
|
|
1490
|
-
if (hasSSE) {
|
|
1491
|
-
lines.push(`use crate::sse::{SseEvent, parse_sse_stream};`)
|
|
1492
|
-
} else {
|
|
1493
|
-
lines.push(`use crate::sse::SseEvent;`)
|
|
1494
|
-
}
|
|
1495
|
-
lines.push(`use crate::ws::TypedWebSocket;`)
|
|
1496
|
-
lines.push(``)
|
|
1497
|
-
return lines
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
function buildRustResources(
|
|
1501
|
-
spec: OpenApiSpec,
|
|
1502
|
-
options: RustSDKOptions,
|
|
1503
|
-
ir: ReturnType<typeof toIR>,
|
|
1504
|
-
): Map<string, string> {
|
|
1505
|
-
const throwOnError = options.throwOnError ?? true
|
|
1506
|
-
const allMethods = collectMethodsFromIR(ir.tree, spec)
|
|
1507
|
-
|
|
1283
|
+
function buildRustResources(root: RustNode, throwOnError: boolean): Map<string, string> {
|
|
1508
1284
|
const files = new Map<string, string>()
|
|
1509
1285
|
|
|
1510
|
-
/* ---- src/resources/mod.rs: one `pub mod` per root namespace ---- */
|
|
1511
1286
|
const modLines: string[] = [`/* Code generated by honey. DO NOT EDIT. */`, ``]
|
|
1512
|
-
for (const
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
const safeName = safeResourceName(seg)
|
|
1517
|
-
modLines.push(`pub use ${rustSnake(safeName)}::${rustPascal(safeName)}Resource;`)
|
|
1518
|
-
const hasSyncDescendants = allMethods.some((m) => !m.isSSE && !m.isWS && m.namePath[0] === seg)
|
|
1519
|
-
if (hasSyncDescendants) {
|
|
1520
|
-
modLines.push(`pub use ${rustSnake(safeName)}::${rustPascal(safeName)}ResourceSync;`)
|
|
1521
|
-
}
|
|
1287
|
+
for (const c of root.children) modLines.push(`pub mod ${c.node.modName};`)
|
|
1288
|
+
for (const c of root.children) {
|
|
1289
|
+
modLines.push(`pub use ${c.node.modName}::${c.node.structName};`)
|
|
1290
|
+
if (hasSync(c.node)) modLines.push(`pub use ${c.node.modName}::${c.node.syncStructName};`)
|
|
1522
1291
|
}
|
|
1523
1292
|
modLines.push(``)
|
|
1524
1293
|
files.set("src/resources/mod.rs", modLines.join("\n"))
|
|
1525
1294
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
const
|
|
1529
|
-
const
|
|
1530
|
-
const
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
const directMethods = methodsOf(ns).map(([, op]) => {
|
|
1534
|
-
const m = allMethods.find((x) => x.opId === op.id)
|
|
1535
|
-
if (!m) throw new Error(`missing MethodInfo for ${op.id}`)
|
|
1536
|
-
return m
|
|
1537
|
-
})
|
|
1538
|
-
|
|
1539
|
-
const childSegs = namespacesOf(ns).map(([seg]) => seg)
|
|
1540
|
-
const hasChildren = childSegs.length > 0
|
|
1541
|
-
|
|
1542
|
-
const fileLines: string[] = [...emitRustResourceFileHeader(directMethods)]
|
|
1543
|
-
|
|
1544
|
-
/* helper: full Pascal struct name for a child at a given path */
|
|
1545
|
-
const childStructName = (childSeg: string): string =>
|
|
1546
|
-
`${[...path, childSeg].map((s) => rustPascal(safeResourceName(s))).join("")}Resource`
|
|
1547
|
-
|
|
1548
|
-
if (hasChildren) {
|
|
1549
|
-
/* sub-modules for child namespaces */
|
|
1550
|
-
for (const childSeg of childSegs) {
|
|
1551
|
-
fileLines.push(`pub mod ${rustSnake(safeResourceName(childSeg))};`)
|
|
1552
|
-
}
|
|
1553
|
-
/* re-export child resource types for convenience */
|
|
1554
|
-
for (const childSeg of childSegs) {
|
|
1555
|
-
const childSafe = safeResourceName(childSeg)
|
|
1556
|
-
fileLines.push(`pub use ${rustSnake(childSafe)}::${childStructName(childSeg)};`)
|
|
1557
|
-
}
|
|
1558
|
-
fileLines.push(``)
|
|
1295
|
+
function emitNode(node: RustNode, dir: string[]): void {
|
|
1296
|
+
const sync = hasSync(node)
|
|
1297
|
+
const l: string[] = [...fileHeader(sync)]
|
|
1298
|
+
for (const c of node.children) l.push(`pub mod ${c.node.modName};`)
|
|
1299
|
+
for (const c of node.children) {
|
|
1300
|
+
l.push(`pub use ${c.node.modName}::${c.node.structName};`)
|
|
1301
|
+
if (hasSync(c.node)) l.push(`pub use ${c.node.modName}::${c.node.syncStructName};`)
|
|
1559
1302
|
}
|
|
1303
|
+
if (node.children.length > 0) l.push(``)
|
|
1560
1304
|
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
/* accessor methods for child namespaces */
|
|
1573
|
-
for (const childSeg of childSegs) {
|
|
1574
|
-
const childSafe = safeResourceName(childSeg)
|
|
1575
|
-
fileLines.push(`\tpub fn ${rustSnake(childSafe)}(&self) -> &${childStructName(childSeg)} {`)
|
|
1576
|
-
fileLines.push(`\t\t&self.${rustSnake(childSafe)}`)
|
|
1577
|
-
fileLines.push(`\t}`)
|
|
1578
|
-
fileLines.push(``)
|
|
1579
|
-
}
|
|
1580
|
-
/* direct methods */
|
|
1581
|
-
for (const m of directMethods.sort((a, b) => a.methodAction.localeCompare(b.methodAction))) {
|
|
1582
|
-
fileLines.push(...emitMethod(m, "self.client", throwOnError).map((l) => `\t${l}`))
|
|
1305
|
+
l.push(`pub struct ${node.structName} {`)
|
|
1306
|
+
l.push(`\tpub(crate) client: std::sync::Arc<crate::client::ClientInner>,`)
|
|
1307
|
+
for (const c of node.children) l.push(`\tpub ${c.accessor}: ${c.node.structName},`)
|
|
1308
|
+
l.push(`}`)
|
|
1309
|
+
l.push(``)
|
|
1310
|
+
l.push(`impl ${node.structName} {`)
|
|
1311
|
+
for (const c of node.children) {
|
|
1312
|
+
l.push(`\tpub fn ${c.accessor}(&self) -> &${c.node.structName} {`)
|
|
1313
|
+
l.push(`\t\t&self.${c.accessor}`)
|
|
1314
|
+
l.push(`\t}`)
|
|
1315
|
+
l.push(``)
|
|
1583
1316
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1317
|
+
for (const plan of node.plans) l.push(...emitMethod(plan, "self.client", throwOnError).map((x) => `\t${x}`))
|
|
1318
|
+
l.push(`}`)
|
|
1319
|
+
l.push(``)
|
|
1320
|
+
l.push(`pub(crate) fn new_resource(client: &std::sync::Arc<crate::client::ClientInner>) -> ${node.structName} {`)
|
|
1321
|
+
l.push(`\t${node.structName} {`)
|
|
1322
|
+
l.push(`\t\tclient: std::sync::Arc::clone(client),`)
|
|
1323
|
+
for (const c of node.children) l.push(`\t\t${c.accessor}: ${c.node.modName}::new_resource(client),`)
|
|
1324
|
+
l.push(`\t}`)
|
|
1325
|
+
l.push(`}`)
|
|
1326
|
+
l.push(``)
|
|
1586
1327
|
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
const
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
fileLines.push(`}`)
|
|
1601
|
-
fileLines.push(``)
|
|
1602
|
-
|
|
1603
|
-
/* sync struct — only when direct non-SSE/non-WS methods exist */
|
|
1604
|
-
const directSyncMethods = directMethods.filter((m) => !m.isSSE && !m.isWS)
|
|
1605
|
-
if (directSyncMethods.length > 0) {
|
|
1606
|
-
fileLines.push(`pub struct ${syncStructName} {`)
|
|
1607
|
-
fileLines.push(`\tpub(crate) client: std::sync::Arc<crate::client::SyncClientInner>,`)
|
|
1608
|
-
fileLines.push(`}`)
|
|
1609
|
-
fileLines.push(``)
|
|
1610
|
-
fileLines.push(`impl ${syncStructName} {`)
|
|
1611
|
-
for (const m of directSyncMethods.sort((a, b) => a.methodAction.localeCompare(b.methodAction))) {
|
|
1612
|
-
fileLines.push(...emitSyncMethod(m, "self.client", throwOnError).map((l) => `\t${l}`))
|
|
1328
|
+
if (sync) {
|
|
1329
|
+
const syncKids = node.children.filter((c) => hasSync(c.node))
|
|
1330
|
+
l.push(`pub struct ${node.syncStructName} {`)
|
|
1331
|
+
l.push(`\tpub(crate) client: std::sync::Arc<crate::client::SyncClientInner>,`)
|
|
1332
|
+
for (const c of syncKids) l.push(`\tpub ${c.accessor}: ${c.node.syncStructName},`)
|
|
1333
|
+
l.push(`}`)
|
|
1334
|
+
l.push(``)
|
|
1335
|
+
l.push(`impl ${node.syncStructName} {`)
|
|
1336
|
+
for (const c of syncKids) {
|
|
1337
|
+
l.push(`\tpub fn ${c.accessor}(&self) -> &${c.node.syncStructName} {`)
|
|
1338
|
+
l.push(`\t\t&self.${c.accessor}`)
|
|
1339
|
+
l.push(`\t}`)
|
|
1340
|
+
l.push(``)
|
|
1613
1341
|
}
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
/* per-op error envelopes */
|
|
1628
|
-
const errEmitted = new Set<string>()
|
|
1629
|
-
for (const m of directMethods.sort((a, b) => a.opId.localeCompare(b.opId))) {
|
|
1630
|
-
if (errEmitted.has(m.opId)) continue
|
|
1631
|
-
errEmitted.add(m.opId)
|
|
1632
|
-
fileLines.push(...emitOperationErrorTypes(m.opId, m.op))
|
|
1342
|
+
for (const plan of node.plans) l.push(...emitSyncMethod(plan, "self.client", throwOnError).map((x) => `\t${x}`))
|
|
1343
|
+
l.push(`}`)
|
|
1344
|
+
l.push(``)
|
|
1345
|
+
l.push(
|
|
1346
|
+
`pub(crate) fn new_resource_sync(client: &std::sync::Arc<crate::client::SyncClientInner>) -> ${node.syncStructName} {`,
|
|
1347
|
+
)
|
|
1348
|
+
l.push(`\t${node.syncStructName} {`)
|
|
1349
|
+
l.push(`\t\tclient: std::sync::Arc::clone(client),`)
|
|
1350
|
+
for (const c of syncKids) l.push(`\t\t${c.accessor}: ${c.node.modName}::new_resource_sync(client),`)
|
|
1351
|
+
l.push(`\t}`)
|
|
1352
|
+
l.push(`}`)
|
|
1353
|
+
l.push(``)
|
|
1633
1354
|
}
|
|
1634
1355
|
|
|
1635
|
-
|
|
1636
|
-
const dirPath = safePath.map(rustSnake).join("/")
|
|
1637
|
-
const filePath = hasChildren ? `src/resources/${dirPath}/mod.rs` : `src/resources/${dirPath}.rs`
|
|
1638
|
-
files.set(filePath, fileLines.join("\n"))
|
|
1356
|
+
for (const plan of node.plans) l.push(...emitOptsStruct(plan), ...emitErrorTypes(plan))
|
|
1639
1357
|
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1358
|
+
const here = [...dir, node.modName]
|
|
1359
|
+
const filePath =
|
|
1360
|
+
node.children.length > 0 ? `src/resources/${here.join("/")}/mod.rs` : `src/resources/${here.join("/")}.rs`
|
|
1361
|
+
files.set(filePath, l.join("\n"))
|
|
1362
|
+
for (const c of node.children) emitNode(c.node, here)
|
|
1644
1363
|
}
|
|
1645
|
-
|
|
1646
|
-
for (const [seg, ns] of namespacesOf(ir.tree)) {
|
|
1647
|
-
emitNsFile(ns, [seg])
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1364
|
+
for (const c of root.children) emitNode(c.node, [])
|
|
1650
1365
|
return files
|
|
1651
1366
|
}
|
|
1652
1367
|
|
|
1653
1368
|
export function generateRustSDK(spec: Record<string, unknown>, options: RustSDKOptions = {}): GeneratedRustSDK {
|
|
1654
1369
|
const crateName = validateCrateName(options.crateName ?? "honey-sdk")
|
|
1370
|
+
const throwOnError = options.throwOnError ?? true
|
|
1655
1371
|
|
|
1656
|
-
|
|
1657
|
-
const
|
|
1658
|
-
|
|
1659
|
-
const
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
const hasRealtime = collectMethodsFromIR(ir.tree, spec as unknown as OpenApiSpec).some((m) => m.isRealtime)
|
|
1372
|
+
const input = spec as unknown as OpenApiSpecInput
|
|
1373
|
+
const model = buildSdkModel(input)
|
|
1374
|
+
const serviceMap = serviceMapOf(model.ir)
|
|
1375
|
+
const names = buildRustNames(model)
|
|
1376
|
+
const hasRealtime = model.ops.some((op) => op.stream === "realtime")
|
|
1377
|
+
const realtimeIds = new Set(model.ops.filter((op) => op.stream === "realtime").map((op) => op.id))
|
|
1663
1378
|
|
|
1664
1379
|
const files: Record<string, string> = {}
|
|
1665
|
-
|
|
1666
|
-
/* copy static runtime files; realtime.rs is conditional; runtime_sync + invalidation_sync always */
|
|
1667
1380
|
for (const [name, content] of loadRustRuntimeTemplates()) {
|
|
1668
|
-
if (name === "realtime.rs")
|
|
1669
|
-
|
|
1670
|
-
} else {
|
|
1671
|
-
files[`src/${name}`] = content
|
|
1672
|
-
}
|
|
1381
|
+
if (name === "realtime.rs" && !hasRealtime) continue
|
|
1382
|
+
files[`src/${name}`] = content
|
|
1673
1383
|
}
|
|
1674
1384
|
|
|
1675
|
-
/* types
|
|
1676
|
-
|
|
1677
|
-
|
|
1385
|
+
/* schema types claim their names first; operations then hoist into the same table */
|
|
1386
|
+
const schemaSource = buildRustTypes(model, names)
|
|
1387
|
+
const { plans, root } = buildTree(model, names, throwOnError)
|
|
1388
|
+
files["src/client.rs"] = buildRustClient(root, plans, throwOnError, model.ir.auth, serviceMap, realtimeIds)
|
|
1389
|
+
for (const [relPath, content] of buildRustResources(root, throwOnError)) files[relPath] = content
|
|
1390
|
+
files["src/types.rs"] = typesFile(schemaSource, names)
|
|
1678
1391
|
files["src/lib.rs"] = buildLibRs(hasRealtime)
|
|
1679
1392
|
files["Cargo.toml"] = buildCargoToml(crateName, {
|
|
1680
|
-
version: options.version ?? "0.1.0",
|
|
1681
1393
|
description: options.description,
|
|
1682
1394
|
homepage: options.homepage,
|
|
1683
|
-
repository: options.repository,
|
|
1684
1395
|
license: options.license,
|
|
1396
|
+
repository: options.repository,
|
|
1397
|
+
version: options.version ?? "0.1.0",
|
|
1685
1398
|
})
|
|
1686
|
-
|
|
1687
|
-
for (const [relPath, content] of buildRustResources(spec as unknown as OpenApiSpec, options, ir)) {
|
|
1688
|
-
files[relPath] = content
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
if (!files["src/resources/mod.rs"]) {
|
|
1692
|
-
files["src/resources/mod.rs"] = `/* Code generated by honey. DO NOT EDIT. */\n`
|
|
1693
|
-
}
|
|
1399
|
+
if (!files["src/resources/mod.rs"]) files["src/resources/mod.rs"] = `/* Code generated by honey. DO NOT EDIT. */\n`
|
|
1694
1400
|
|
|
1695
1401
|
return { files, serviceMap }
|
|
1696
1402
|
}
|