From 92e9f00e97d12f7b52b299083a1fc4418cb23bb7 Mon Sep 17 00:00:00 2001 From: Jacob Quinn Date: Wed, 5 Aug 2026 21:55:16 -0600 Subject: [PATCH 1/9] feat(package)!: rebuild production OpenAPI Replace the legacy generated-client and server implementation with the normalized OpenAPI 3.0, 3.1, and 3.2 pipeline. Keep the provisional JSON Schema engine isolated inside OpenAPI. Generate clients against that engine until its API is ready to move upstream. Keep HTTP optional through an extension. Leave server framework integration to downstream packages such as Servo. Add adversarial, conformance, external-corpus, runtime HTTP, and JuliaC trim-compilation coverage. BREAKING CHANGE: The legacy OpenAPI 0.2 API is replaced by the namespaced document and client-generation API. --- .github/workflows/CI.yml | 40 + .github/workflows/TagBot.yml | 14 +- .github/workflows/ci.yml | 65 - .gitignore | 9 +- AGENTS.md | 138 + CONTRIBUTING.md | 16 - LICENSE.md | 40 +- Project.toml | 44 +- README.md | 359 +- SKILL.md | 129 + codecov.yml | 2 - docs/.gitignore | 1 - docs/Project.toml | 5 - docs/make.jl | 24 - docs/src/index.md | 12 - docs/src/reference.md | 68 - docs/src/todo.md | 14 - docs/src/tools.md | 146 - docs/src/userguide.md | 231 -- ext/OpenAPIHTTPExt.jl | 60 + schemas/README.md | 14 + schemas/oas-3.0.json | 1651 ++++++++ schemas/oas-3.1.json | 1411 +++++++ schemas/oas-3.2.json | 1684 ++++++++ src/OpenAPI.jl | 65 +- src/client.jl | 3369 +++++++++++++++-- src/client/chunk_readers.jl | 149 - src/client/clienttypes.jl | 224 -- src/client/httplibs/httplibs.jl | 60 - src/client/httplibs/julialang_downloads.jl | 242 -- src/client/httplibs/juliaweb_http.jl | 419 -- src/commontypes.jl | 22 - src/datetime.jl | 74 - src/diagnostics.jl | 122 + src/document.jl | 260 ++ src/json.jl | 176 - src/loading.jl | 470 +++ src/normalize.jl | 1898 ++++++++++ src/planning.jl | 1554 ++++++++ src/read.jl | 44 + src/references.jl | 403 ++ src/runtime.jl | 9 + src/schema_engine/README.md | 14 + src/schema_engine/SchemaEngine.jl | 20 + src/schema_engine/compiled.jl | 1558 ++++++++ src/schema_engine/compiled_validation.jl | 1219 ++++++ src/schema_engine/dialects.jl | 293 ++ src/schema_engine/issues.jl | 39 + src/schema_engine/rebase.jl | 270 ++ src/schema_engine/resources.jl | 927 +++++ src/schemas.jl | 93 + src/server.jl | 186 - src/source_locations.jl | 302 ++ src/tools.jl | 311 -- src/val.jl | 88 - test/.gitignore | 1 - test/Project.toml | 16 + test/chunkreader_tests.jl | 352 -- .../AllAnyClient/.openapi-generator-ignore | 23 - .../AllAnyClient/.openapi-generator/FILES | 25 - .../AllAnyClient/.openapi-generator/VERSION | 1 - test/client/allany/AllAnyClient/README.md | 55 - .../allany/AllAnyClient/docs/AnyOfBaseType.md | 16 - .../AllAnyClient/docs/AnyOfMappedPets.md | 19 - .../allany/AllAnyClient/docs/AnyOfPets.md | 16 - test/client/allany/AllAnyClient/docs/Cat.md | 14 - .../allany/AllAnyClient/docs/CatAllOf.md | 13 - .../allany/AllAnyClient/docs/DefaultApi.md | 211 -- test/client/allany/AllAnyClient/docs/Dog.md | 14 - .../allany/AllAnyClient/docs/DogAllOf.md | 13 - .../allany/AllAnyClient/docs/OneOfBaseType.md | 15 - .../AllAnyClient/docs/OneOfMappedPets.md | 18 - .../allany/AllAnyClient/docs/OneOfPets.md | 15 - test/client/allany/AllAnyClient/docs/Pet.md | 12 - .../docs/TypeWithAllArrayTypes.md | 15 - .../allany/AllAnyClient/src/AllAnyClient.jl | 16 - .../AllAnyClient/src/apis/api_DefaultApi.jl | 202 - .../allany/AllAnyClient/src/modelincludes.jl | 13 - .../src/models/model_AnyOfBaseType.jl | 20 - .../src/models/model_AnyOfMappedPets.jl | 24 - .../src/models/model_AnyOfPets.jl | 24 - .../AllAnyClient/src/models/model_Cat.jl | 47 - .../AllAnyClient/src/models/model_CatAllOf.jl | 35 - .../AllAnyClient/src/models/model_Dog.jl | 51 - .../AllAnyClient/src/models/model_DogAllOf.jl | 38 - .../src/models/model_OneOfBaseType.jl | 20 - .../src/models/model_OneOfMappedPets.jl | 24 - .../src/models/model_OneOfPets.jl | 24 - .../AllAnyClient/src/models/model_Pet.jl | 37 - .../src/models/model_TypeWithAllArrayTypes.jl | 51 - test/client/allany/generate.sh | 5 - test/client/allany/runtests.jl | 186 - .../openapigenerator_petstore_v3/generate.sh | 7 - .../petstore/.openapi-generator-ignore | 23 - .../petstore/.openapi-generator/FILES | 21 - .../petstore/.openapi-generator/VERSION | 1 - .../petstore/README.md | 96 - .../petstore/docs/ApiResponse.md | 14 - .../petstore/docs/Category.md | 13 - .../petstore/docs/Order.md | 17 - .../petstore/docs/Pet.md | 17 - .../petstore/docs/PetApi.md | 276 -- .../petstore/docs/StoreApi.md | 128 - .../petstore/docs/Tag.md | 13 - .../petstore/docs/User.md | 19 - .../petstore/docs/UserApi.md | 254 -- .../petstore/src/OpenAPIGenPetStoreClient.jl | 31 - .../petstore/src/apis/api_PetApi.jl | 285 -- .../petstore/src/apis/api_StoreApi.jl | 145 - .../petstore/src/apis/api_UserApi.jl | 274 -- .../petstore/src/modelincludes.jl | 9 - .../petstore/src/models/model_ApiResponse.jl | 50 - .../petstore/src/models/model_Category.jl | 48 - .../petstore/src/models/model_Order.jl | 78 - .../petstore/src/models/model_Pet.jl | 71 - .../petstore/src/models/model_Tag.jl | 45 - .../petstore/src/models/model_User.jl | 78 - .../petstore_test_petapi.jl | 77 - .../petstore_test_storeapi.jl | 74 - .../petstore_test_userapi.jl | 193 - .../openapigenerator_petstore_v3/runtests.jl | 20 - test/client/param_serialize.jl | 45 - test/client/petstore_v2/generate.sh | 7 - .../petstore/.openapi-generator-ignore | 23 - .../petstore/.openapi-generator/FILES | 21 - .../petstore/.openapi-generator/VERSION | 1 - test/client/petstore_v2/petstore/README.md | 96 - .../petstore_v2/petstore/docs/ApiResponse.md | 14 - .../petstore_v2/petstore/docs/Category.md | 13 - .../client/petstore_v2/petstore/docs/Order.md | 17 - test/client/petstore_v2/petstore/docs/Pet.md | 17 - .../petstore_v2/petstore/docs/PetApi.md | 266 -- .../petstore_v2/petstore/docs/StoreApi.md | 126 - test/client/petstore_v2/petstore/docs/Tag.md | 13 - test/client/petstore_v2/petstore/docs/User.md | 19 - .../petstore_v2/petstore/docs/UserApi.md | 244 -- .../petstore/src/PetStoreClient.jl | 31 - .../petstore/src/apis/api_PetApi.jl | 274 -- .../petstore/src/apis/api_StoreApi.jl | 145 - .../petstore/src/apis/api_UserApi.jl | 262 -- .../petstore_v2/petstore/src/modelincludes.jl | 9 - .../petstore/src/models/model_ApiResponse.jl | 49 - .../petstore/src/models/model_Category.jl | 44 - .../petstore/src/models/model_Order.jl | 77 - .../petstore/src/models/model_Pet.jl | 70 - .../petstore/src/models/model_Tag.jl | 44 - .../petstore/src/models/model_User.jl | 77 - .../petstore_v2/petstore_test_petapi.jl | 67 - .../petstore_v2/petstore_test_storeapi.jl | 73 - .../petstore_v2/petstore_test_userapi.jl | 179 - test/client/petstore_v2/runtests.jl | 44 - .../petstore_v2/start_petstore_server.sh | 4 - .../petstore_v2/stop_petstore_server.sh | 4 - test/client/petstore_v3/generate.sh | 7 - .../petstore/.openapi-generator-ignore | 23 - .../petstore/.openapi-generator/FILES | 21 - .../petstore/.openapi-generator/VERSION | 1 - test/client/petstore_v3/petstore/README.md | 96 - .../petstore_v3/petstore/docs/ApiResponse.md | 14 - .../petstore_v3/petstore/docs/Category.md | 13 - .../client/petstore_v3/petstore/docs/Order.md | 17 - test/client/petstore_v3/petstore/docs/Pet.md | 17 - .../petstore_v3/petstore/docs/PetApi.md | 266 -- .../petstore_v3/petstore/docs/StoreApi.md | 126 - test/client/petstore_v3/petstore/docs/Tag.md | 13 - test/client/petstore_v3/petstore/docs/User.md | 19 - .../petstore_v3/petstore/docs/UserApi.md | 244 -- .../petstore/src/PetStoreClient.jl | 31 - .../petstore/src/apis/api_PetApi.jl | 273 -- .../petstore/src/apis/api_StoreApi.jl | 143 - .../petstore/src/apis/api_UserApi.jl | 262 -- .../petstore_v3/petstore/src/modelincludes.jl | 9 - .../petstore/src/models/model_ApiResponse.jl | 50 - .../petstore/src/models/model_Category.jl | 45 - .../petstore/src/models/model_Order.jl | 78 - .../petstore/src/models/model_Pet.jl | 71 - .../petstore/src/models/model_Tag.jl | 45 - .../petstore/src/models/model_User.jl | 78 - .../petstore_v3/petstore_test_petapi.jl | 77 - .../petstore_v3/petstore_test_storeapi.jl | 74 - .../petstore_v3/petstore_test_userapi.jl | 193 - test/client/petstore_v3/runtests.jl | 44 - .../petstore_v3/start_petstore_server.sh | 4 - .../petstore_v3/stop_petstore_server.sh | 4 - test/client/runtests.jl | 58 - .../.openapi-generator-ignore | 23 - .../.openapi-generator/FILES | 7 - .../.openapi-generator/VERSION | 1 - .../timeouttest/TimeoutTestClient/README.md | 41 - .../TimeoutTestClient/docs/DefaultApi.md | 66 - .../docs/DelayresponseGet200Response.md | 12 - .../src/TimeoutTestClient.jl | 16 - .../src/apis/api_DefaultApi.jl | 77 - .../TimeoutTestClient/src/modelincludes.jl | 4 - .../model_DelayresponseGet200Response.jl | 36 - test/client/timeouttest/generate.sh | 5 - test/client/timeouttest/runtests.jl | 78 - test/client/utilstests.jl | 343 -- test/corpus.jl | 131 + .../DeepClient/.openapi-generator-ignore | 23 - .../DeepClient/.openapi-generator/FILES | 9 - .../DeepClient/.openapi-generator/VERSION | 1 - test/deep_object/DeepClient/Project.toml | 17 - test/deep_object/DeepClient/README.md | 40 - .../docs/FindPetsByStatus200Response.md | 12 - .../docs/FindPetsByStatus200ResponseResult.md | 13 - .../docs/FindPetsByStatusStatusParameter.md | 13 - ...etsByStatusStatusParameterStatusesInner.md | 12 - ...StatusStatusParameterStatusesInnerInner.md | 12 - test/deep_object/DeepClient/docs/PetApi.md | 39 - test/deep_object/DeepClient/src/DeepClient.jl | 16 - .../DeepClient/src/apis/api_PetApi.jl | 46 - test/deep_object/DeepClient/src/deep.jl | 16 - .../DeepClient/src/modelincludes.jl | 5 - .../model_FindPetsByStatus200Response.jl | 30 - ...model_FindPetsByStatus200ResponseResult.jl | 34 - .../model_FindPetsByStatusStatusParameter.jl | 34 - ...etsByStatusStatusParameterStatusesInner.jl | 30 - ...StatusStatusParameterStatusesInnerInner.jl | 30 - .../DeepServer/.openapi-generator-ignore | 23 - .../DeepServer/.openapi-generator/FILES | 9 - .../DeepServer/.openapi-generator/VERSION | 1 - test/deep_object/DeepServer/Project.toml | 17 - test/deep_object/DeepServer/README.md | 58 - .../docs/FindPetsByStatus200Response.md | 12 - .../docs/FindPetsByStatus200ResponseResult.md | 13 - .../docs/FindPetsByStatusStatusParameter.md | 13 - ...etsByStatusStatusParameterStatusesInner.md | 12 - ...StatusStatusParameterStatusesInnerInner.md | 12 - test/deep_object/DeepServer/docs/PetApi.md | 38 - test/deep_object/DeepServer/src/DeepServer.jl | 54 - .../DeepServer/src/apis/api_PetApi.jl | 37 - .../DeepServer/src/modelincludes.jl | 5 - .../model_FindPetsByStatus200Response.jl | 29 - ...model_FindPetsByStatus200ResponseResult.jl | 33 - .../model_FindPetsByStatusStatusParameter.jl | 33 - ...etsByStatusStatusParameterStatusesInner.jl | 29 - ...StatusStatusParameterStatusesInnerInner.jl | 29 - test/deep_object/deep.yaml | 62 - test/deep_object/deep_client.jl | 25 - test/deep_object/deep_server.jl | 38 - test/discriminators.jl | 265 ++ .../FormsClient/.openapi-generator-ignore | 23 - .../FormsClient/.openapi-generator/FILES | 7 - .../FormsClient/.openapi-generator/VERSION | 1 - test/forms/FormsClient/README.md | 42 - test/forms/FormsClient/docs/DefaultApi.md | 116 - test/forms/FormsClient/docs/TestResponse.md | 12 - test/forms/FormsClient/src/FormsClient.jl | 16 - .../FormsClient/src/apis/api_DefaultApi.jl | 115 - test/forms/FormsClient/src/modelincludes.jl | 4 - .../src/models/model_TestResponse.jl | 36 - .../FormsServer/.openapi-generator-ignore | 23 - .../FormsServer/.openapi-generator/FILES | 7 - .../FormsServer/.openapi-generator/VERSION | 1 - test/forms/FormsServer/README.md | 60 - test/forms/FormsServer/docs/DefaultApi.md | 113 - test/forms/FormsServer/docs/TestResponse.md | 12 - test/forms/FormsServer/src/FormsServer.jl | 60 - .../FormsServer/src/apis/api_DefaultApi.jl | 213 -- test/forms/FormsServer/src/modelincludes.jl | 4 - .../src/models/model_TestResponse.jl | 36 - test/forms/forms_client.jl | 48 - test/forms/forms_server.jl | 51 - test/forms/generate.sh | 10 - .../ModelGenClient/.openapi-generator-ignore | 23 - .../ModelGenClient/.openapi-generator/FILES | 9 - .../ModelGenClient/.openapi-generator/VERSION | 1 - test/modelgen/ModelGenClient/README.md | 40 - .../ModelGenClient/docs/ComputeType.md | 11 - .../ModelGenClient/docs/DefaultApi.md | 33 - .../ModelGenClient/docs/SchemaIsEnum.md | 11 - .../modelgen/ModelGenClient/docs/TestModel.md | 18 - .../ModelGenClient/src/ModelGenClient.jl | 16 - .../ModelGenClient/src/apis/api_DefaultApi.jl | 41 - .../ModelGenClient/src/modelincludes.jl | 5 - .../src/models/model_ComputeType.jl | 9 - .../src/models/model_TestModel.jl | 67 - .../ModelGenServer/.openapi-generator-ignore | 23 - .../ModelGenServer/.openapi-generator/FILES | 9 - .../ModelGenServer/.openapi-generator/VERSION | 1 - test/modelgen/ModelGenServer/README.md | 58 - .../ModelGenServer/docs/ComputeType.md | 11 - .../ModelGenServer/docs/DefaultApi.md | 32 - .../ModelGenServer/docs/SchemaIsEnum.md | 11 - .../modelgen/ModelGenServer/docs/TestModel.md | 18 - .../ModelGenServer/src/ModelGenServer.jl | 54 - .../ModelGenServer/src/apis/api_DefaultApi.jl | 35 - .../ModelGenServer/src/modelincludes.jl | 5 - .../src/models/model_ComputeType.jl | 9 - .../src/models/model_TestModel.jl | 67 - test/modelgen/generate.sh | 10 - test/modelgen/testmodelgen.jl | 22 - test/models.jl | 201 + test/normalization.jl | 503 +++ test/opa/OPAServer/src/apis/api_DataAPIApi.jl | 205 - test/openapi_trim_workload.jl | 73 + test/param_deserialize.jl | 130 - test/references.jl | 555 +++ test/runtests.jl | 459 +-- test/runtime.jl | 226 ++ test/runtime_integration.jl | 878 +++++ test/schema_engine/compiled.jl | 599 +++ test/schema_engine/official.jl | 83 + test/schema_engine/rebase.jl | 103 + test/schema_engine/resources.jl | 215 ++ test/semantics.jl | 445 +++ .../AllAnyServer/.openapi-generator-ignore | 23 - .../AllAnyServer/.openapi-generator/FILES | 25 - .../AllAnyServer/.openapi-generator/VERSION | 1 - test/server/allany/AllAnyServer/README.md | 73 - .../allany/AllAnyServer/docs/AnyOfBaseType.md | 16 - .../AllAnyServer/docs/AnyOfMappedPets.md | 19 - .../allany/AllAnyServer/docs/AnyOfPets.md | 16 - test/server/allany/AllAnyServer/docs/Cat.md | 14 - .../allany/AllAnyServer/docs/CatAllOf.md | 13 - .../allany/AllAnyServer/docs/DefaultApi.md | 204 - test/server/allany/AllAnyServer/docs/Dog.md | 14 - .../allany/AllAnyServer/docs/DogAllOf.md | 13 - .../allany/AllAnyServer/docs/OneOfBaseType.md | 15 - .../AllAnyServer/docs/OneOfMappedPets.md | 18 - .../allany/AllAnyServer/docs/OneOfPets.md | 15 - test/server/allany/AllAnyServer/docs/Pet.md | 12 - .../docs/TypeWithAllArrayTypes.md | 15 - .../allany/AllAnyServer/src/AllAnyServer.jl | 72 - .../AllAnyServer/src/apis/api_DefaultApi.jl | 295 -- .../allany/AllAnyServer/src/modelincludes.jl | 13 - .../src/models/model_AnyOfBaseType.jl | 20 - .../src/models/model_AnyOfMappedPets.jl | 24 - .../src/models/model_AnyOfPets.jl | 24 - .../AllAnyServer/src/models/model_Cat.jl | 47 - .../AllAnyServer/src/models/model_CatAllOf.jl | 33 - .../AllAnyServer/src/models/model_Dog.jl | 51 - .../AllAnyServer/src/models/model_DogAllOf.jl | 36 - .../src/models/model_OneOfBaseType.jl | 20 - .../src/models/model_OneOfMappedPets.jl | 24 - .../src/models/model_OneOfPets.jl | 24 - .../AllAnyServer/src/models/model_Pet.jl | 37 - .../src/models/model_TypeWithAllArrayTypes.jl | 51 - test/server/allany/allany_server.jl | 98 - test/server/allany/generate.sh | 1 - .../src/apis/api_JobRunnerApi.jl | 298 -- .../openapigenerator_petstore_v3/generate.sh | 6 - .../petstore/.openapi-generator-ignore | 23 - .../petstore/.openapi-generator/FILES | 21 - .../petstore/.openapi-generator/VERSION | 1 - .../petstore/README.md | 82 - .../petstore/docs/ApiResponse.md | 14 - .../petstore/docs/Category.md | 13 - .../petstore/docs/Order.md | 17 - .../petstore/docs/Pet.md | 17 - .../petstore/docs/PetApi.md | 268 -- .../petstore/docs/StoreApi.md | 124 - .../petstore/docs/Tag.md | 13 - .../petstore/docs/User.md | 19 - .../petstore/docs/UserApi.md | 246 -- .../petstore/src/OpenAPIGenPetStoreServer.jl | 123 - .../petstore/src/apis/api_PetApi.jl | 407 -- .../petstore/src/apis/api_StoreApi.jl | 157 - .../petstore/src/apis/api_UserApi.jl | 348 -- .../petstore/src/modelincludes.jl | 9 - .../petstore/src/models/model_ApiResponse.jl | 50 - .../petstore/src/models/model_Category.jl | 48 - .../petstore/src/models/model_Order.jl | 78 - .../petstore/src/models/model_Pet.jl | 71 - .../petstore/src/models/model_Tag.jl | 45 - .../petstore/src/models/model_User.jl | 78 - .../petstore_server.jl | 169 - test/server/petstore_v2/generate.sh | 6 - .../petstore/.openapi-generator-ignore | 23 - .../petstore/.openapi-generator/FILES | 21 - .../petstore/.openapi-generator/VERSION | 1 - test/server/petstore_v2/petstore/README.md | 82 - .../petstore_v2/petstore/docs/ApiResponse.md | 14 - .../petstore_v2/petstore/docs/Category.md | 13 - .../server/petstore_v2/petstore/docs/Order.md | 17 - test/server/petstore_v2/petstore/docs/Pet.md | 17 - .../petstore_v2/petstore/docs/PetApi.md | 258 -- .../petstore_v2/petstore/docs/StoreApi.md | 122 - test/server/petstore_v2/petstore/docs/Tag.md | 13 - test/server/petstore_v2/petstore/docs/User.md | 19 - .../petstore_v2/petstore/docs/UserApi.md | 236 -- .../petstore/src/PetStoreServer.jl | 123 - .../petstore/src/apis/api_PetApi.jl | 407 -- .../petstore/src/apis/api_StoreApi.jl | 152 - .../petstore/src/apis/api_UserApi.jl | 353 -- .../petstore_v2/petstore/src/modelincludes.jl | 9 - .../petstore/src/models/model_ApiResponse.jl | 49 - .../petstore/src/models/model_Category.jl | 44 - .../petstore/src/models/model_Order.jl | 77 - .../petstore/src/models/model_Pet.jl | 70 - .../petstore/src/models/model_Tag.jl | 44 - .../petstore/src/models/model_User.jl | 77 - test/server/petstore_v2/petstore_server.jl | 170 - test/server/petstore_v3/generate.sh | 6 - .../petstore/.openapi-generator-ignore | 23 - .../petstore/.openapi-generator/FILES | 21 - .../petstore/.openapi-generator/VERSION | 1 - test/server/petstore_v3/petstore/README.md | 82 - .../petstore_v3/petstore/docs/ApiResponse.md | 14 - .../petstore_v3/petstore/docs/Category.md | 13 - .../server/petstore_v3/petstore/docs/Order.md | 17 - test/server/petstore_v3/petstore/docs/Pet.md | 17 - .../petstore_v3/petstore/docs/PetApi.md | 258 -- .../petstore_v3/petstore/docs/StoreApi.md | 122 - test/server/petstore_v3/petstore/docs/Tag.md | 13 - test/server/petstore_v3/petstore/docs/User.md | 19 - .../petstore_v3/petstore/docs/UserApi.md | 236 -- .../petstore/src/PetStoreServer.jl | 123 - .../petstore/src/apis/api_PetApi.jl | 407 -- .../petstore/src/apis/api_StoreApi.jl | 157 - .../petstore/src/apis/api_UserApi.jl | 353 -- .../petstore_v3/petstore/src/modelincludes.jl | 9 - .../petstore/src/models/model_ApiResponse.jl | 50 - .../petstore/src/models/model_Category.jl | 45 - .../petstore/src/models/model_Order.jl | 78 - .../petstore/src/models/model_Pet.jl | 71 - .../petstore/src/models/model_Tag.jl | 45 - .../petstore/src/models/model_User.jl | 78 - test/server/petstore_v3/petstore_server.jl | 169 - .../.openapi-generator-ignore | 23 - .../.openapi-generator/FILES | 7 - .../.openapi-generator/VERSION | 1 - .../timeouttest/TimeoutTestServer/README.md | 59 - .../TimeoutTestServer/docs/DefaultApi.md | 64 - .../docs/DelayresponseGet200Response.md | 12 - .../src/TimeoutTestServer.jl | 57 - .../src/apis/api_DefaultApi.jl | 82 - .../TimeoutTestServer/src/modelincludes.jl | 4 - .../model_DelayresponseGet200Response.jl | 36 - test/server/timeouttest/generate.sh | 5 - test/server/timeouttest/timeouttest_server.jl | 68 - test/specs/allany.yaml | 211 -- test/specs/forms.json | 195 - test/specs/modelgen.json | 78 - test/specs/openapigenerator_petstore_v3.json | 1132 ------ test/specs/petstore_v2.json | 1054 ------ test/specs/petstore_v3.json | 1265 ------- test/specs/stresstest.yaml | 56 - test/specs/timeouttest.yaml | 55 - test/streaming_latency_tests.jl | 110 - test/stresstest/README.md | 34 - test/stresstest/StressTest/StressTest.jl | 24 - test/stresstest/StressTest/execution.jl | 134 - test/stresstest/StressTest/metrics.jl | 306 -- .../StressTestClient/src/StressTestClient.jl | 16 - .../src/apis/api_DefaultApi.jl | 74 - .../StressTestClient/src/modelincludes.jl | 6 - .../src/models/model_EchoGet200Response.jl | 44 - .../src/models/model_EchoPost200Response.jl | 44 - .../src/models/model_EchoPostRequest.jl | 36 - test/stresstest/generate.sh | 5 - test/stresstest/runtests.jl | 90 - test/stresstest/stresstest_server.jl | 100 - test/testutils.jl | 68 - test/trim/Project.toml | 5 + test/trim/fixture.jl | 116 + test/trim_compile_tests.jl | 169 + 458 files changed, 22933 insertions(+), 30236 deletions(-) create mode 100644 .github/workflows/CI.yml delete mode 100644 .github/workflows/ci.yml create mode 100644 AGENTS.md delete mode 100644 CONTRIBUTING.md create mode 100644 SKILL.md delete mode 100644 codecov.yml delete mode 100644 docs/.gitignore delete mode 100644 docs/Project.toml delete mode 100644 docs/make.jl delete mode 100644 docs/src/index.md delete mode 100644 docs/src/reference.md delete mode 100644 docs/src/todo.md delete mode 100644 docs/src/tools.md delete mode 100644 docs/src/userguide.md create mode 100644 ext/OpenAPIHTTPExt.jl create mode 100644 schemas/README.md create mode 100644 schemas/oas-3.0.json create mode 100644 schemas/oas-3.1.json create mode 100644 schemas/oas-3.2.json delete mode 100644 src/client/chunk_readers.jl delete mode 100644 src/client/clienttypes.jl delete mode 100644 src/client/httplibs/httplibs.jl delete mode 100644 src/client/httplibs/julialang_downloads.jl delete mode 100644 src/client/httplibs/juliaweb_http.jl delete mode 100644 src/commontypes.jl delete mode 100644 src/datetime.jl create mode 100644 src/diagnostics.jl create mode 100644 src/document.jl delete mode 100644 src/json.jl create mode 100644 src/loading.jl create mode 100644 src/normalize.jl create mode 100644 src/planning.jl create mode 100644 src/read.jl create mode 100644 src/references.jl create mode 100644 src/runtime.jl create mode 100644 src/schema_engine/README.md create mode 100644 src/schema_engine/SchemaEngine.jl create mode 100644 src/schema_engine/compiled.jl create mode 100644 src/schema_engine/compiled_validation.jl create mode 100644 src/schema_engine/dialects.jl create mode 100644 src/schema_engine/issues.jl create mode 100644 src/schema_engine/rebase.jl create mode 100644 src/schema_engine/resources.jl create mode 100644 src/schemas.jl delete mode 100644 src/server.jl create mode 100644 src/source_locations.jl delete mode 100644 src/tools.jl delete mode 100644 src/val.jl delete mode 100644 test/.gitignore create mode 100644 test/Project.toml delete mode 100644 test/chunkreader_tests.jl delete mode 100644 test/client/allany/AllAnyClient/.openapi-generator-ignore delete mode 100644 test/client/allany/AllAnyClient/.openapi-generator/FILES delete mode 100644 test/client/allany/AllAnyClient/.openapi-generator/VERSION delete mode 100644 test/client/allany/AllAnyClient/README.md delete mode 100644 test/client/allany/AllAnyClient/docs/AnyOfBaseType.md delete mode 100644 test/client/allany/AllAnyClient/docs/AnyOfMappedPets.md delete mode 100644 test/client/allany/AllAnyClient/docs/AnyOfPets.md delete mode 100644 test/client/allany/AllAnyClient/docs/Cat.md delete mode 100644 test/client/allany/AllAnyClient/docs/CatAllOf.md delete mode 100644 test/client/allany/AllAnyClient/docs/DefaultApi.md delete mode 100644 test/client/allany/AllAnyClient/docs/Dog.md delete mode 100644 test/client/allany/AllAnyClient/docs/DogAllOf.md delete mode 100644 test/client/allany/AllAnyClient/docs/OneOfBaseType.md delete mode 100644 test/client/allany/AllAnyClient/docs/OneOfMappedPets.md delete mode 100644 test/client/allany/AllAnyClient/docs/OneOfPets.md delete mode 100644 test/client/allany/AllAnyClient/docs/Pet.md delete mode 100644 test/client/allany/AllAnyClient/docs/TypeWithAllArrayTypes.md delete mode 100644 test/client/allany/AllAnyClient/src/AllAnyClient.jl delete mode 100644 test/client/allany/AllAnyClient/src/apis/api_DefaultApi.jl delete mode 100644 test/client/allany/AllAnyClient/src/modelincludes.jl delete mode 100644 test/client/allany/AllAnyClient/src/models/model_AnyOfBaseType.jl delete mode 100644 test/client/allany/AllAnyClient/src/models/model_AnyOfMappedPets.jl delete mode 100644 test/client/allany/AllAnyClient/src/models/model_AnyOfPets.jl delete mode 100644 test/client/allany/AllAnyClient/src/models/model_Cat.jl delete mode 100644 test/client/allany/AllAnyClient/src/models/model_CatAllOf.jl delete mode 100644 test/client/allany/AllAnyClient/src/models/model_Dog.jl delete mode 100644 test/client/allany/AllAnyClient/src/models/model_DogAllOf.jl delete mode 100644 test/client/allany/AllAnyClient/src/models/model_OneOfBaseType.jl delete mode 100644 test/client/allany/AllAnyClient/src/models/model_OneOfMappedPets.jl delete mode 100644 test/client/allany/AllAnyClient/src/models/model_OneOfPets.jl delete mode 100644 test/client/allany/AllAnyClient/src/models/model_Pet.jl delete mode 100644 test/client/allany/AllAnyClient/src/models/model_TypeWithAllArrayTypes.jl delete mode 100755 test/client/allany/generate.sh delete mode 100644 test/client/allany/runtests.jl delete mode 100755 test/client/openapigenerator_petstore_v3/generate.sh delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/.openapi-generator-ignore delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/.openapi-generator/FILES delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/.openapi-generator/VERSION delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/README.md delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/docs/ApiResponse.md delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/docs/Category.md delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/docs/Order.md delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/docs/Pet.md delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/docs/PetApi.md delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/docs/StoreApi.md delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/docs/Tag.md delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/docs/User.md delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/docs/UserApi.md delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/src/OpenAPIGenPetStoreClient.jl delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/src/apis/api_PetApi.jl delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/src/apis/api_StoreApi.jl delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/src/apis/api_UserApi.jl delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/src/modelincludes.jl delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/src/models/model_ApiResponse.jl delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/src/models/model_Category.jl delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/src/models/model_Order.jl delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/src/models/model_Pet.jl delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/src/models/model_Tag.jl delete mode 100644 test/client/openapigenerator_petstore_v3/petstore/src/models/model_User.jl delete mode 100644 test/client/openapigenerator_petstore_v3/petstore_test_petapi.jl delete mode 100644 test/client/openapigenerator_petstore_v3/petstore_test_storeapi.jl delete mode 100644 test/client/openapigenerator_petstore_v3/petstore_test_userapi.jl delete mode 100644 test/client/openapigenerator_petstore_v3/runtests.jl delete mode 100644 test/client/param_serialize.jl delete mode 100755 test/client/petstore_v2/generate.sh delete mode 100644 test/client/petstore_v2/petstore/.openapi-generator-ignore delete mode 100644 test/client/petstore_v2/petstore/.openapi-generator/FILES delete mode 100644 test/client/petstore_v2/petstore/.openapi-generator/VERSION delete mode 100644 test/client/petstore_v2/petstore/README.md delete mode 100644 test/client/petstore_v2/petstore/docs/ApiResponse.md delete mode 100644 test/client/petstore_v2/petstore/docs/Category.md delete mode 100644 test/client/petstore_v2/petstore/docs/Order.md delete mode 100644 test/client/petstore_v2/petstore/docs/Pet.md delete mode 100644 test/client/petstore_v2/petstore/docs/PetApi.md delete mode 100644 test/client/petstore_v2/petstore/docs/StoreApi.md delete mode 100644 test/client/petstore_v2/petstore/docs/Tag.md delete mode 100644 test/client/petstore_v2/petstore/docs/User.md delete mode 100644 test/client/petstore_v2/petstore/docs/UserApi.md delete mode 100644 test/client/petstore_v2/petstore/src/PetStoreClient.jl delete mode 100644 test/client/petstore_v2/petstore/src/apis/api_PetApi.jl delete mode 100644 test/client/petstore_v2/petstore/src/apis/api_StoreApi.jl delete mode 100644 test/client/petstore_v2/petstore/src/apis/api_UserApi.jl delete mode 100644 test/client/petstore_v2/petstore/src/modelincludes.jl delete mode 100644 test/client/petstore_v2/petstore/src/models/model_ApiResponse.jl delete mode 100644 test/client/petstore_v2/petstore/src/models/model_Category.jl delete mode 100644 test/client/petstore_v2/petstore/src/models/model_Order.jl delete mode 100644 test/client/petstore_v2/petstore/src/models/model_Pet.jl delete mode 100644 test/client/petstore_v2/petstore/src/models/model_Tag.jl delete mode 100644 test/client/petstore_v2/petstore/src/models/model_User.jl delete mode 100644 test/client/petstore_v2/petstore_test_petapi.jl delete mode 100644 test/client/petstore_v2/petstore_test_storeapi.jl delete mode 100644 test/client/petstore_v2/petstore_test_userapi.jl delete mode 100644 test/client/petstore_v2/runtests.jl delete mode 100755 test/client/petstore_v2/start_petstore_server.sh delete mode 100755 test/client/petstore_v2/stop_petstore_server.sh delete mode 100755 test/client/petstore_v3/generate.sh delete mode 100644 test/client/petstore_v3/petstore/.openapi-generator-ignore delete mode 100644 test/client/petstore_v3/petstore/.openapi-generator/FILES delete mode 100644 test/client/petstore_v3/petstore/.openapi-generator/VERSION delete mode 100644 test/client/petstore_v3/petstore/README.md delete mode 100644 test/client/petstore_v3/petstore/docs/ApiResponse.md delete mode 100644 test/client/petstore_v3/petstore/docs/Category.md delete mode 100644 test/client/petstore_v3/petstore/docs/Order.md delete mode 100644 test/client/petstore_v3/petstore/docs/Pet.md delete mode 100644 test/client/petstore_v3/petstore/docs/PetApi.md delete mode 100644 test/client/petstore_v3/petstore/docs/StoreApi.md delete mode 100644 test/client/petstore_v3/petstore/docs/Tag.md delete mode 100644 test/client/petstore_v3/petstore/docs/User.md delete mode 100644 test/client/petstore_v3/petstore/docs/UserApi.md delete mode 100644 test/client/petstore_v3/petstore/src/PetStoreClient.jl delete mode 100644 test/client/petstore_v3/petstore/src/apis/api_PetApi.jl delete mode 100644 test/client/petstore_v3/petstore/src/apis/api_StoreApi.jl delete mode 100644 test/client/petstore_v3/petstore/src/apis/api_UserApi.jl delete mode 100644 test/client/petstore_v3/petstore/src/modelincludes.jl delete mode 100644 test/client/petstore_v3/petstore/src/models/model_ApiResponse.jl delete mode 100644 test/client/petstore_v3/petstore/src/models/model_Category.jl delete mode 100644 test/client/petstore_v3/petstore/src/models/model_Order.jl delete mode 100644 test/client/petstore_v3/petstore/src/models/model_Pet.jl delete mode 100644 test/client/petstore_v3/petstore/src/models/model_Tag.jl delete mode 100644 test/client/petstore_v3/petstore/src/models/model_User.jl delete mode 100644 test/client/petstore_v3/petstore_test_petapi.jl delete mode 100644 test/client/petstore_v3/petstore_test_storeapi.jl delete mode 100644 test/client/petstore_v3/petstore_test_userapi.jl delete mode 100644 test/client/petstore_v3/runtests.jl delete mode 100755 test/client/petstore_v3/start_petstore_server.sh delete mode 100755 test/client/petstore_v3/stop_petstore_server.sh delete mode 100644 test/client/runtests.jl delete mode 100644 test/client/timeouttest/TimeoutTestClient/.openapi-generator-ignore delete mode 100644 test/client/timeouttest/TimeoutTestClient/.openapi-generator/FILES delete mode 100644 test/client/timeouttest/TimeoutTestClient/.openapi-generator/VERSION delete mode 100644 test/client/timeouttest/TimeoutTestClient/README.md delete mode 100644 test/client/timeouttest/TimeoutTestClient/docs/DefaultApi.md delete mode 100644 test/client/timeouttest/TimeoutTestClient/docs/DelayresponseGet200Response.md delete mode 100644 test/client/timeouttest/TimeoutTestClient/src/TimeoutTestClient.jl delete mode 100644 test/client/timeouttest/TimeoutTestClient/src/apis/api_DefaultApi.jl delete mode 100644 test/client/timeouttest/TimeoutTestClient/src/modelincludes.jl delete mode 100644 test/client/timeouttest/TimeoutTestClient/src/models/model_DelayresponseGet200Response.jl delete mode 100755 test/client/timeouttest/generate.sh delete mode 100644 test/client/timeouttest/runtests.jl delete mode 100644 test/client/utilstests.jl create mode 100644 test/corpus.jl delete mode 100644 test/deep_object/DeepClient/.openapi-generator-ignore delete mode 100644 test/deep_object/DeepClient/.openapi-generator/FILES delete mode 100644 test/deep_object/DeepClient/.openapi-generator/VERSION delete mode 100644 test/deep_object/DeepClient/Project.toml delete mode 100644 test/deep_object/DeepClient/README.md delete mode 100644 test/deep_object/DeepClient/docs/FindPetsByStatus200Response.md delete mode 100644 test/deep_object/DeepClient/docs/FindPetsByStatus200ResponseResult.md delete mode 100644 test/deep_object/DeepClient/docs/FindPetsByStatusStatusParameter.md delete mode 100644 test/deep_object/DeepClient/docs/FindPetsByStatusStatusParameterStatusesInner.md delete mode 100644 test/deep_object/DeepClient/docs/FindPetsByStatusStatusParameterStatusesInnerInner.md delete mode 100644 test/deep_object/DeepClient/docs/PetApi.md delete mode 100644 test/deep_object/DeepClient/src/DeepClient.jl delete mode 100644 test/deep_object/DeepClient/src/apis/api_PetApi.jl delete mode 100644 test/deep_object/DeepClient/src/deep.jl delete mode 100644 test/deep_object/DeepClient/src/modelincludes.jl delete mode 100644 test/deep_object/DeepClient/src/models/model_FindPetsByStatus200Response.jl delete mode 100644 test/deep_object/DeepClient/src/models/model_FindPetsByStatus200ResponseResult.jl delete mode 100644 test/deep_object/DeepClient/src/models/model_FindPetsByStatusStatusParameter.jl delete mode 100644 test/deep_object/DeepClient/src/models/model_FindPetsByStatusStatusParameterStatusesInner.jl delete mode 100644 test/deep_object/DeepClient/src/models/model_FindPetsByStatusStatusParameterStatusesInnerInner.jl delete mode 100644 test/deep_object/DeepServer/.openapi-generator-ignore delete mode 100644 test/deep_object/DeepServer/.openapi-generator/FILES delete mode 100644 test/deep_object/DeepServer/.openapi-generator/VERSION delete mode 100644 test/deep_object/DeepServer/Project.toml delete mode 100644 test/deep_object/DeepServer/README.md delete mode 100644 test/deep_object/DeepServer/docs/FindPetsByStatus200Response.md delete mode 100644 test/deep_object/DeepServer/docs/FindPetsByStatus200ResponseResult.md delete mode 100644 test/deep_object/DeepServer/docs/FindPetsByStatusStatusParameter.md delete mode 100644 test/deep_object/DeepServer/docs/FindPetsByStatusStatusParameterStatusesInner.md delete mode 100644 test/deep_object/DeepServer/docs/FindPetsByStatusStatusParameterStatusesInnerInner.md delete mode 100644 test/deep_object/DeepServer/docs/PetApi.md delete mode 100644 test/deep_object/DeepServer/src/DeepServer.jl delete mode 100644 test/deep_object/DeepServer/src/apis/api_PetApi.jl delete mode 100644 test/deep_object/DeepServer/src/modelincludes.jl delete mode 100644 test/deep_object/DeepServer/src/models/model_FindPetsByStatus200Response.jl delete mode 100644 test/deep_object/DeepServer/src/models/model_FindPetsByStatus200ResponseResult.jl delete mode 100644 test/deep_object/DeepServer/src/models/model_FindPetsByStatusStatusParameter.jl delete mode 100644 test/deep_object/DeepServer/src/models/model_FindPetsByStatusStatusParameterStatusesInner.jl delete mode 100644 test/deep_object/DeepServer/src/models/model_FindPetsByStatusStatusParameterStatusesInnerInner.jl delete mode 100644 test/deep_object/deep.yaml delete mode 100644 test/deep_object/deep_client.jl delete mode 100644 test/deep_object/deep_server.jl create mode 100644 test/discriminators.jl delete mode 100644 test/forms/FormsClient/.openapi-generator-ignore delete mode 100644 test/forms/FormsClient/.openapi-generator/FILES delete mode 100644 test/forms/FormsClient/.openapi-generator/VERSION delete mode 100644 test/forms/FormsClient/README.md delete mode 100644 test/forms/FormsClient/docs/DefaultApi.md delete mode 100644 test/forms/FormsClient/docs/TestResponse.md delete mode 100644 test/forms/FormsClient/src/FormsClient.jl delete mode 100644 test/forms/FormsClient/src/apis/api_DefaultApi.jl delete mode 100644 test/forms/FormsClient/src/modelincludes.jl delete mode 100644 test/forms/FormsClient/src/models/model_TestResponse.jl delete mode 100644 test/forms/FormsServer/.openapi-generator-ignore delete mode 100644 test/forms/FormsServer/.openapi-generator/FILES delete mode 100644 test/forms/FormsServer/.openapi-generator/VERSION delete mode 100644 test/forms/FormsServer/README.md delete mode 100644 test/forms/FormsServer/docs/DefaultApi.md delete mode 100644 test/forms/FormsServer/docs/TestResponse.md delete mode 100644 test/forms/FormsServer/src/FormsServer.jl delete mode 100644 test/forms/FormsServer/src/apis/api_DefaultApi.jl delete mode 100644 test/forms/FormsServer/src/modelincludes.jl delete mode 100644 test/forms/FormsServer/src/models/model_TestResponse.jl delete mode 100644 test/forms/forms_client.jl delete mode 100644 test/forms/forms_server.jl delete mode 100755 test/forms/generate.sh delete mode 100644 test/modelgen/ModelGenClient/.openapi-generator-ignore delete mode 100644 test/modelgen/ModelGenClient/.openapi-generator/FILES delete mode 100644 test/modelgen/ModelGenClient/.openapi-generator/VERSION delete mode 100644 test/modelgen/ModelGenClient/README.md delete mode 100644 test/modelgen/ModelGenClient/docs/ComputeType.md delete mode 100644 test/modelgen/ModelGenClient/docs/DefaultApi.md delete mode 100644 test/modelgen/ModelGenClient/docs/SchemaIsEnum.md delete mode 100644 test/modelgen/ModelGenClient/docs/TestModel.md delete mode 100644 test/modelgen/ModelGenClient/src/ModelGenClient.jl delete mode 100644 test/modelgen/ModelGenClient/src/apis/api_DefaultApi.jl delete mode 100644 test/modelgen/ModelGenClient/src/modelincludes.jl delete mode 100644 test/modelgen/ModelGenClient/src/models/model_ComputeType.jl delete mode 100644 test/modelgen/ModelGenClient/src/models/model_TestModel.jl delete mode 100644 test/modelgen/ModelGenServer/.openapi-generator-ignore delete mode 100644 test/modelgen/ModelGenServer/.openapi-generator/FILES delete mode 100644 test/modelgen/ModelGenServer/.openapi-generator/VERSION delete mode 100644 test/modelgen/ModelGenServer/README.md delete mode 100644 test/modelgen/ModelGenServer/docs/ComputeType.md delete mode 100644 test/modelgen/ModelGenServer/docs/DefaultApi.md delete mode 100644 test/modelgen/ModelGenServer/docs/SchemaIsEnum.md delete mode 100644 test/modelgen/ModelGenServer/docs/TestModel.md delete mode 100644 test/modelgen/ModelGenServer/src/ModelGenServer.jl delete mode 100644 test/modelgen/ModelGenServer/src/apis/api_DefaultApi.jl delete mode 100644 test/modelgen/ModelGenServer/src/modelincludes.jl delete mode 100644 test/modelgen/ModelGenServer/src/models/model_ComputeType.jl delete mode 100644 test/modelgen/ModelGenServer/src/models/model_TestModel.jl delete mode 100755 test/modelgen/generate.sh delete mode 100644 test/modelgen/testmodelgen.jl create mode 100644 test/models.jl create mode 100644 test/normalization.jl delete mode 100644 test/opa/OPAServer/src/apis/api_DataAPIApi.jl create mode 100644 test/openapi_trim_workload.jl delete mode 100644 test/param_deserialize.jl create mode 100644 test/references.jl create mode 100644 test/runtime.jl create mode 100644 test/runtime_integration.jl create mode 100644 test/schema_engine/compiled.jl create mode 100644 test/schema_engine/official.jl create mode 100644 test/schema_engine/rebase.jl create mode 100644 test/schema_engine/resources.jl create mode 100644 test/semantics.jl delete mode 100644 test/server/allany/AllAnyServer/.openapi-generator-ignore delete mode 100644 test/server/allany/AllAnyServer/.openapi-generator/FILES delete mode 100644 test/server/allany/AllAnyServer/.openapi-generator/VERSION delete mode 100644 test/server/allany/AllAnyServer/README.md delete mode 100644 test/server/allany/AllAnyServer/docs/AnyOfBaseType.md delete mode 100644 test/server/allany/AllAnyServer/docs/AnyOfMappedPets.md delete mode 100644 test/server/allany/AllAnyServer/docs/AnyOfPets.md delete mode 100644 test/server/allany/AllAnyServer/docs/Cat.md delete mode 100644 test/server/allany/AllAnyServer/docs/CatAllOf.md delete mode 100644 test/server/allany/AllAnyServer/docs/DefaultApi.md delete mode 100644 test/server/allany/AllAnyServer/docs/Dog.md delete mode 100644 test/server/allany/AllAnyServer/docs/DogAllOf.md delete mode 100644 test/server/allany/AllAnyServer/docs/OneOfBaseType.md delete mode 100644 test/server/allany/AllAnyServer/docs/OneOfMappedPets.md delete mode 100644 test/server/allany/AllAnyServer/docs/OneOfPets.md delete mode 100644 test/server/allany/AllAnyServer/docs/Pet.md delete mode 100644 test/server/allany/AllAnyServer/docs/TypeWithAllArrayTypes.md delete mode 100644 test/server/allany/AllAnyServer/src/AllAnyServer.jl delete mode 100644 test/server/allany/AllAnyServer/src/apis/api_DefaultApi.jl delete mode 100644 test/server/allany/AllAnyServer/src/modelincludes.jl delete mode 100644 test/server/allany/AllAnyServer/src/models/model_AnyOfBaseType.jl delete mode 100644 test/server/allany/AllAnyServer/src/models/model_AnyOfMappedPets.jl delete mode 100644 test/server/allany/AllAnyServer/src/models/model_AnyOfPets.jl delete mode 100644 test/server/allany/AllAnyServer/src/models/model_Cat.jl delete mode 100644 test/server/allany/AllAnyServer/src/models/model_CatAllOf.jl delete mode 100644 test/server/allany/AllAnyServer/src/models/model_Dog.jl delete mode 100644 test/server/allany/AllAnyServer/src/models/model_DogAllOf.jl delete mode 100644 test/server/allany/AllAnyServer/src/models/model_OneOfBaseType.jl delete mode 100644 test/server/allany/AllAnyServer/src/models/model_OneOfMappedPets.jl delete mode 100644 test/server/allany/AllAnyServer/src/models/model_OneOfPets.jl delete mode 100644 test/server/allany/AllAnyServer/src/models/model_Pet.jl delete mode 100644 test/server/allany/AllAnyServer/src/models/model_TypeWithAllArrayTypes.jl delete mode 100644 test/server/allany/allany_server.jl delete mode 100755 test/server/allany/generate.sh delete mode 100644 test/server/juliahub/specs/juliahubrunner/src/apis/api_JobRunnerApi.jl delete mode 100755 test/server/openapigenerator_petstore_v3/generate.sh delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/.openapi-generator-ignore delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/.openapi-generator/FILES delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/.openapi-generator/VERSION delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/README.md delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/docs/ApiResponse.md delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/docs/Category.md delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/docs/Order.md delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/docs/Pet.md delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/docs/PetApi.md delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/docs/StoreApi.md delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/docs/Tag.md delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/docs/User.md delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/docs/UserApi.md delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/src/OpenAPIGenPetStoreServer.jl delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/src/apis/api_PetApi.jl delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/src/apis/api_StoreApi.jl delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/src/apis/api_UserApi.jl delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/src/modelincludes.jl delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/src/models/model_ApiResponse.jl delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/src/models/model_Category.jl delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/src/models/model_Order.jl delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/src/models/model_Pet.jl delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/src/models/model_Tag.jl delete mode 100644 test/server/openapigenerator_petstore_v3/petstore/src/models/model_User.jl delete mode 100644 test/server/openapigenerator_petstore_v3/petstore_server.jl delete mode 100755 test/server/petstore_v2/generate.sh delete mode 100644 test/server/petstore_v2/petstore/.openapi-generator-ignore delete mode 100644 test/server/petstore_v2/petstore/.openapi-generator/FILES delete mode 100644 test/server/petstore_v2/petstore/.openapi-generator/VERSION delete mode 100644 test/server/petstore_v2/petstore/README.md delete mode 100644 test/server/petstore_v2/petstore/docs/ApiResponse.md delete mode 100644 test/server/petstore_v2/petstore/docs/Category.md delete mode 100644 test/server/petstore_v2/petstore/docs/Order.md delete mode 100644 test/server/petstore_v2/petstore/docs/Pet.md delete mode 100644 test/server/petstore_v2/petstore/docs/PetApi.md delete mode 100644 test/server/petstore_v2/petstore/docs/StoreApi.md delete mode 100644 test/server/petstore_v2/petstore/docs/Tag.md delete mode 100644 test/server/petstore_v2/petstore/docs/User.md delete mode 100644 test/server/petstore_v2/petstore/docs/UserApi.md delete mode 100644 test/server/petstore_v2/petstore/src/PetStoreServer.jl delete mode 100644 test/server/petstore_v2/petstore/src/apis/api_PetApi.jl delete mode 100644 test/server/petstore_v2/petstore/src/apis/api_StoreApi.jl delete mode 100644 test/server/petstore_v2/petstore/src/apis/api_UserApi.jl delete mode 100644 test/server/petstore_v2/petstore/src/modelincludes.jl delete mode 100644 test/server/petstore_v2/petstore/src/models/model_ApiResponse.jl delete mode 100644 test/server/petstore_v2/petstore/src/models/model_Category.jl delete mode 100644 test/server/petstore_v2/petstore/src/models/model_Order.jl delete mode 100644 test/server/petstore_v2/petstore/src/models/model_Pet.jl delete mode 100644 test/server/petstore_v2/petstore/src/models/model_Tag.jl delete mode 100644 test/server/petstore_v2/petstore/src/models/model_User.jl delete mode 100644 test/server/petstore_v2/petstore_server.jl delete mode 100755 test/server/petstore_v3/generate.sh delete mode 100644 test/server/petstore_v3/petstore/.openapi-generator-ignore delete mode 100644 test/server/petstore_v3/petstore/.openapi-generator/FILES delete mode 100644 test/server/petstore_v3/petstore/.openapi-generator/VERSION delete mode 100644 test/server/petstore_v3/petstore/README.md delete mode 100644 test/server/petstore_v3/petstore/docs/ApiResponse.md delete mode 100644 test/server/petstore_v3/petstore/docs/Category.md delete mode 100644 test/server/petstore_v3/petstore/docs/Order.md delete mode 100644 test/server/petstore_v3/petstore/docs/Pet.md delete mode 100644 test/server/petstore_v3/petstore/docs/PetApi.md delete mode 100644 test/server/petstore_v3/petstore/docs/StoreApi.md delete mode 100644 test/server/petstore_v3/petstore/docs/Tag.md delete mode 100644 test/server/petstore_v3/petstore/docs/User.md delete mode 100644 test/server/petstore_v3/petstore/docs/UserApi.md delete mode 100644 test/server/petstore_v3/petstore/src/PetStoreServer.jl delete mode 100644 test/server/petstore_v3/petstore/src/apis/api_PetApi.jl delete mode 100644 test/server/petstore_v3/petstore/src/apis/api_StoreApi.jl delete mode 100644 test/server/petstore_v3/petstore/src/apis/api_UserApi.jl delete mode 100644 test/server/petstore_v3/petstore/src/modelincludes.jl delete mode 100644 test/server/petstore_v3/petstore/src/models/model_ApiResponse.jl delete mode 100644 test/server/petstore_v3/petstore/src/models/model_Category.jl delete mode 100644 test/server/petstore_v3/petstore/src/models/model_Order.jl delete mode 100644 test/server/petstore_v3/petstore/src/models/model_Pet.jl delete mode 100644 test/server/petstore_v3/petstore/src/models/model_Tag.jl delete mode 100644 test/server/petstore_v3/petstore/src/models/model_User.jl delete mode 100644 test/server/petstore_v3/petstore_server.jl delete mode 100644 test/server/timeouttest/TimeoutTestServer/.openapi-generator-ignore delete mode 100644 test/server/timeouttest/TimeoutTestServer/.openapi-generator/FILES delete mode 100644 test/server/timeouttest/TimeoutTestServer/.openapi-generator/VERSION delete mode 100644 test/server/timeouttest/TimeoutTestServer/README.md delete mode 100644 test/server/timeouttest/TimeoutTestServer/docs/DefaultApi.md delete mode 100644 test/server/timeouttest/TimeoutTestServer/docs/DelayresponseGet200Response.md delete mode 100644 test/server/timeouttest/TimeoutTestServer/src/TimeoutTestServer.jl delete mode 100644 test/server/timeouttest/TimeoutTestServer/src/apis/api_DefaultApi.jl delete mode 100644 test/server/timeouttest/TimeoutTestServer/src/modelincludes.jl delete mode 100644 test/server/timeouttest/TimeoutTestServer/src/models/model_DelayresponseGet200Response.jl delete mode 100755 test/server/timeouttest/generate.sh delete mode 100644 test/server/timeouttest/timeouttest_server.jl delete mode 100644 test/specs/allany.yaml delete mode 100644 test/specs/forms.json delete mode 100644 test/specs/modelgen.json delete mode 100644 test/specs/openapigenerator_petstore_v3.json delete mode 100644 test/specs/petstore_v2.json delete mode 100644 test/specs/petstore_v3.json delete mode 100644 test/specs/stresstest.yaml delete mode 100644 test/specs/timeouttest.yaml delete mode 100644 test/streaming_latency_tests.jl delete mode 100644 test/stresstest/README.md delete mode 100644 test/stresstest/StressTest/StressTest.jl delete mode 100644 test/stresstest/StressTest/execution.jl delete mode 100644 test/stresstest/StressTest/metrics.jl delete mode 100644 test/stresstest/StressTestClient/src/StressTestClient.jl delete mode 100644 test/stresstest/StressTestClient/src/apis/api_DefaultApi.jl delete mode 100644 test/stresstest/StressTestClient/src/modelincludes.jl delete mode 100644 test/stresstest/StressTestClient/src/models/model_EchoGet200Response.jl delete mode 100644 test/stresstest/StressTestClient/src/models/model_EchoPost200Response.jl delete mode 100644 test/stresstest/StressTestClient/src/models/model_EchoPostRequest.jl delete mode 100755 test/stresstest/generate.sh delete mode 100644 test/stresstest/runtests.jl delete mode 100644 test/stresstest/stresstest_server.jl delete mode 100644 test/testutils.jl create mode 100644 test/trim/Project.toml create mode 100644 test/trim/fixture.jl create mode 100644 test/trim_compile_tests.jl diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..7550e8a --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,40 @@ +name: CI +on: + push: + branches: [main] + tags: ["*"] + pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + test: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + version: ['1.11', '1'] + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.version }} + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 + corpus: + name: OpenAPI corpus + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + OPENAPI_CORPUS_TESTS: ${{ startsWith(github.ref, 'refs/tags/') && 'all' || 'small' }} + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: '1' + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml index f49313b..fd05b28 100644 --- a/.github/workflows/TagBot.yml +++ b/.github/workflows/TagBot.yml @@ -8,8 +8,20 @@ jobs: TagBot: if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' runs-on: ubuntu-latest + permissions: + actions: read + checks: read + contents: write + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + security-events: read + statuses: read steps: - uses: JuliaRegistries/TagBot@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - ssh: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 09f331b..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: CI -on: - push: - branches: [main] - tags: ["*"] - pull_request: -jobs: - test: - name: Julia ${{ matrix.version }} - HTTP ${{ matrix.http }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - version: - - '1.6' - - '1' # automatically expands to the latest stable 1.x release of Julia - - 'nightly' - http: - - '1' - - '2' - os: - - ubuntu-latest - arch: - - x64 - exclude: - # HTTP.jl 2.0 requires Julia >= 1.10 - - version: '1.6' - http: '2' - steps: - - uses: actions/checkout@v4 - - uses: julia-actions/setup-julia@v1 - with: - version: ${{ matrix.version }} - arch: ${{ matrix.arch }} - - uses: actions/cache@v4 - env: - cache-name: cache-artifacts - with: - path: ~/.julia/artifacts - key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} - restore-keys: | - ${{ runner.os }}-test-${{ env.cache-name }}- - ${{ runner.os }}-test- - ${{ runner.os }}- - - name: Constrain HTTP.jl to v${{ matrix.http }} - run: sed -i -E '/^\[compat\]/,/^\[/{s/^HTTP = .*/HTTP = "${{ matrix.http }}"/}' Project.toml - - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-runtest@v1 - - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 - with: - file: lcov.info - docs: - name: Documentation - runs-on: ubuntu-latest - permissions: - contents: write - statuses: write - steps: - - uses: actions/checkout@v4 - - uses: julia-actions/julia-buildpkg@latest - - uses: julia-actions/julia-docdeploy@latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.gitignore b/.gitignore index 2ba67bd..dbd8fbc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ -Manifest.toml -.vscode \ No newline at end of file +*.jl.cov +*.jl.*.cov +*.jl.mem +.DS_Store +/Manifest.toml +/Manifest-v*.toml +/test/Manifest.toml diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..eb6d810 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,138 @@ +# OpenAPI.jl maintainer guide + +## Purpose + +OpenAPI.jl has two related surfaces: + +1. It reads OpenAPI 3.0, 3.1, and 3.2 descriptions and generates typed Julia + clients. +2. It creates a smaller OpenAPI 3.2 document from declared Julia operations. + +The client generator must fail before code emission when it cannot preserve the +specified wire behavior. Do not generate a plausible but incorrect client. + +## Architecture + +The client pipeline has strict stage boundaries: + +```text +source + -> load and structural validation + -> reference resolution and normalization + -> typed client planning + -> deterministic Julia source generation + -> generated runtime validation and HTTP transport +``` + +The main files are: + +- `src/loading.jl`: JSON/YAML parsing, official OAS schema checks, source + identity, limits, and source locations. +- `src/schema_engine/`: provisional generic JSON Schema resources, references, + dialects, compilation, rebasing, and validation. Keep this directory free of + OpenAPI-specific behavior so it can move upstream later. +- `src/references.jl`: bounded OpenAPI object reference resolution and the + adapter to the internal schema engine. +- `src/normalize.jl`: immutable, version-neutral OpenAPI intermediate forms and + semantic checks. +- `src/planning.jl`: Julia names, model shapes, operation signatures, and + explicit generation support checks. +- `src/client.jl`: source emission and the runtime embedded in generated + clients. +- `src/schemas.jl`, `src/document.jl`: Julia type schema mapping and the smaller + document-authoring API. +- `ext/OpenAPIHTTPExt.jl`: HTTP document retrieval. + +## Design rules + +- Keep the package export surface empty. Users call `OpenAPI.load`, + `OpenAPI.normalize`, `OpenAPI.plan`, and `OpenAPI.client` through the module. +- Keep parsed and normalized documents immutable. Do not mutate caller-owned + input. +- Keep diagnostics stable and machine-readable. Include a resource and JSON + Pointer location. +- Keep network and file reference access bounded. Preserve same-origin and + explicit-root defaults. +- Keep generic URI, resource, pointer, reference, dialect, compilation, and + validation behavior in `src/schema_engine`. Add OpenAPI-specific rules only + outside that directory. +- Treat `src/schema_engine` as provisional. Preserve its extraction boundary + so the code can move to JSONSchema.jl after it hardens. +- Do not add a dependency on Servo or another server framework. Downstream + packages own router integration through package extensions. +- Put generic strict JSON parsing behavior in JSON.jl when it belongs there. +- Treat the normative OpenAPI text as authoritative over published structural + schemas. +- Preserve distinct missing, explicit-null, request, and response model + semantics. +- Keep generation deterministic. Sort unordered document maps before they + affect names or emitted source. +- Do not silently approximate unsupported wire behavior. The known deliberate + planning failures are OAS 3.2 `querystring` parameters and streaming or + positional `itemSchema`, `itemEncoding`, and `prefixEncoding` behavior. + +## Public types and functions + +- `SourceDocument`, `Diagnostic`, `OpenAPIError`: loading and diagnostics. +- `NormalizedAPI`: immutable normalized document. +- `ClientPlan`: deterministic generation plan. +- `load`, `check`, `normalize`, `plan`, `client`: client pipeline. +- `Param`, `Operation`, `document`: declaration-based authoring API. +- `register!`, `operations`: extension seams implemented by downstream server + packages. + +Generated modules have their own public runtime types. Important types include +`Client`, credential types, `Upload`, `MultipartPartHeaders`, `ApiResponse`, +`ApiError`, `SchemaValidationError`, `Absent`, and `ABSENT`. + +## Tests + +Run the package tests on both supported Julia minor lines: + +```sh +julia +1.12 --project=. -e 'using Pkg; Pkg.test()' +julia +1.11 --project=. -e 'using Pkg; Pkg.test()' +``` + +The focused files separate concerns: + +- `test/normalization.jl`: loading, versions, immutability, generated source. +- `test/references.jl`: external, anchor, recursive, and cyclic references. +- `test/semantics.jl`: OpenAPI semantic rules and explicit deferrals. +- `test/models.jl`, `test/discriminators.jl`: schema-to-type edge cases. +- `test/runtime.jl`: generated runtime units. +- `test/runtime_integration.jl`: live local HTTP behavior. +- `test/schema_engine/`: direct resource, compilation, validation, and rebasing + coverage for the provisional schema engine. +- `test/corpus.jl`: pinned public API descriptions. + +Run corpus checks separately: + +```sh +OPENAPI_CORPUS_TESTS=small julia --project=. -e 'using Pkg; Pkg.test()' +OPENAPI_CORPUS_TESTS=all julia --project=. -e 'using Pkg; Pkg.test()' +``` + +When a test changes expected wire bytes, inspect the bytes or HTTP request. A +source-compilation check alone is not enough. + +## Dependency development + +OpenAPI.jl requires JSON.jl 1.7. The additional schema engine is currently +internal. Changes to it need direct tests and full OpenAPI corpus validation. +Do not couple its internals to OpenAPI normalization or planning. + +## Common change workflow + +1. Add or pin a specification example that demonstrates the behavior. +2. Add a normalized semantic test before changing generated source. +3. Add runtime or live HTTP coverage when bytes, headers, security, or response + selection change. +4. Run focused tests. +5. Run the full Julia 1.12 and 1.11 suites. +6. Run the small corpus. Run the full corpus for reference, planning, naming, + schema, or generation changes. +7. Check `git diff --check` and inspect generated source for deterministic + output. + +Do not push or publish without direct user approval. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 804fb7a..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,16 +0,0 @@ -# Guidelines For Contributing - -### Updating the code generator - -The ["openapi-generator"](https://github.com/OpenAPITools/openapi-generator/) repository contains the code generator for Julia. For any changes that also need updates to the generated code, a PR needs to be made to the `openapi-generator` repo. Relevant files: -- -- -- -- -- -- -- -- -- -- - diff --git a/LICENSE.md b/LICENSE.md index 026ca48..5ba278f 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,22 +1,22 @@ The OpenAPI.jl package is licensed under the MIT "Expat" License: -Copyright (c) 2022: Julia Computing Inc. All rights reserved. - -> Permission is hereby granted, free of charge, to any person obtaining a copy -> of this software and associated documentation files (the "Software"), to deal -> in the Software without restriction, including without limitation the rights -> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -> copies of the Software, and to permit persons to whom the Software is -> furnished to do so, subject to the following conditions: -> -> The above copyright notice and this permission notice shall be included in all -> copies or substantial portions of the Software. -> -> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -> SOFTWARE. -> +> Copyright (c) 2026: Jacob Quinn, JuliaServices contributors. +> +> Permission is hereby granted, free of charge, to any person obtaining +> a copy of this software and associated documentation files (the +> "Software"), to deal in the Software without restriction, including +> without limitation the rights to use, copy, modify, merge, publish, +> distribute, sublicense, and/or sell copies of the Software, and to +> permit persons to whom the Software is furnished to do so, subject to +> the following conditions: +> +> The above copyright notice and this permission notice shall be +> included in all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +> IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +> CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +> TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +> SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Project.toml b/Project.toml index 25b6e82..f15fc5c 100644 --- a/Project.toml +++ b/Project.toml @@ -4,41 +4,25 @@ keywords = ["Swagger", "OpenAPI", "REST"] license = "MIT" desc = "OpenAPI server and client helper for Julia" authors = ["JuliaHub Inc."] -version = "0.2.7" +version = "1.0.0" [deps] -Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" -Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" -HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -LibCURL = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" -MIMEs = "6c6e2e6c-3030-632d-7369-2d6c69616d65" -MbedTLS = "739be429-bea8-5141-9913-cc70e7f3736d" -TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" +SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" -p7zip_jll = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" - -[compat] -Downloads = "1" -HTTP = "1, 2" -JSON = "0.20, 0.21, 1" -LibCURL = "0.6, 1" -MIMEs = "0.1, 1" -MbedTLS = "0.6.8, 0.7, 1" -TimeZones = "1" -URIs = "1.3" -julia = "1.6" -p7zip_jll = "17" +YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" -[extras] -Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +[weakdeps] HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" -Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" -URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" -[targets] -test = ["Test", "Random", "URIs", "HTTP", "Dates", "TimeZones", "Sockets"] +[extensions] +OpenAPIHTTPExt = "HTTP" + +[compat] +Dates = "1.11" +HTTP = "2" +JSON = "1.7" +URIs = "1" +YAML = "0.4" +julia = "1.11" diff --git a/README.md b/README.md index 267b674..ed97712 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,346 @@ -# OpenAPI +# OpenAPI.jl -[![Build Status](https://github.com/JuliaComputing/OpenAPI.jl/workflows/CI/badge.svg)](https://github.com/JuliaComputing/OpenAPI.jl/actions?query=workflow%3ACI+branch%3Amain) -[![codecov](https://codecov.io/gh/JuliaComputing/OpenAPI.jl/branch/main/graph/badge.svg?token=iZeFL7Js0l)](https://codecov.io/gh/JuliaComputing/OpenAPI.jl) +OpenAPI.jl reads OpenAPI descriptions and generates single-file, typed Julia +HTTP clients. It also provides a smaller API for creating an OpenAPI document +from declared Julia endpoints. -This is the Julia library needed along with code generated by the [OpenAPI generator](https://openapi-generator.tech/) to help define, produce and consume OpenAPI interfaces. +The client pipeline supports OpenAPI 3.0.x, 3.1.x, and 3.2.x. It parses JSON +and YAML, resolves references, validates the document, normalizes version +differences, plans Julia types, and emits deterministic source code. -[![](https://img.shields.io/badge/docs-latest-blue.svg)](https://JuliaComputing.github.io/OpenAPI.jl) +Generated schema graphs use content-derived resource identifiers. Local paths, +source URL userinfo, and source URL query strings are not embedded in generated +files. A relative Server Object still depends on the public scheme, host, and +path of the source URL because that location is part of the OpenAPI resolution +rule. -## Quick Guide +OpenAPI.jl does not export names. Use its API through the `OpenAPI` namespace. -- Create an API specification. Check out [OpenAPI-Spec](https://github.com/OAI/OpenAPI-Specification) for specification syntax and examples. -- Use [instructions](https://openapi-generator.tech/docs/generators) provided for the Julia OpenAPI code generator plugin to generate Julia code. -- Client: - - Use the generated client in Julia directly to invoke APIs -- Server: - - Provide code to handle API invocations on the server side by implementing the Julia methods corresponding to API stubs. - - Start a server using HTTP.jl and register the generated request handlers. +## Generate a client -## Examples +Load `HTTP` before reading a URL. Local files and inline JSON or YAML do not +need `HTTP` during generation. -The Petstore is a common example that most OpenAPI implementations use to test and demonstrate. Clients and servers generated from both version 2 and 3 specifications are included in this repo. +```julia +using OpenAPI, HTTP -- Petstore v2: - - Client: [docs](test/client/petstore_v2/petstore/README.md), [implementation](test/client/petstore_v2) - - Server: [docs](test/server/petstore_v2/petstore/README.md), [implementation](test/server/petstore_v2) -- Petstore v3: - - Client: [docs](test/client/petstore_v3/petstore/README.md), [implementation](test/client/petstore_v3) - - Server: [docs](test/server/petstore_v3/petstore/README.md), [implementation](test/server/petstore_v3) +source = OpenAPI.load("https://example.com/openapi.yaml") +api = OpenAPI.normalize(source) +plan = OpenAPI.plan(api; name = "ExampleClient") +OpenAPI.client(plan; path = "ExampleClient.jl") +``` + +The short form runs the same pipeline: + +```julia +using OpenAPI, HTTP + +OpenAPI.client( + "https://example.com/openapi.yaml"; + name = "ExampleClient", + path = "ExampleClient.jl", +) +``` + +The generated file imports `OpenAPI`, `HTTP`, and `JSON`. It also imports the +Julia standard libraries `Base64`, `Dates`, and `UUIDs`. Add the three package +dependencies to the environment that will include the generated file. + +```julia +include("ExampleClient.jl") + +client = ExampleClient.Client( + "https://api.example.com"; + headers = ["User-Agent" => "my-app/1.0"], +) + +# Each operationId becomes a Julia function. Path parameters are positional. +# Other parameters are keywords. A required request body is the last positional +# argument. Pass `client=client` to avoid shared global configuration. +result = ExampleClient.get_widget("widget-123"; verbose = true, client) +``` + +Optional model fields use `ExampleClient.Absent`, not `nothing`. This keeps a +missing value distinct from an explicit JSON `null`. + +```julia +model = ExampleClient.WidgetInput( + name = "example", + description = ExampleClient.ABSENT, +) +``` + +Pass `with_http_info=true` to receive an `ApiResponse` with the status, raw +headers, decoded documented headers, and typed body. A non-2xx response throws +`ApiError`. The error keeps the raw body even when documented error decoding +fails. + +## Pipeline and diagnostics + +The public stages are separate so applications can inspect or cache them. + +- `OpenAPI.load(source)` parses one root document and validates it against the + official schema for its OAS minor line. It returns an immutable + `SourceDocument` with source identity, format, version, and source locations. +- `OpenAPI.check(source)` returns structured `Diagnostic` values instead of + throwing for document validation errors. +- `OpenAPI.normalize(source)` resolves references and creates an immutable, + version-neutral `NormalizedAPI`. +- `OpenAPI.plan(source; name="ApiClient")` creates deterministic Julia model + and operation plans. +- `OpenAPI.client(source; ...)` emits source and optionally writes it to a + file. + +Errors use stable diagnostic codes and resource plus JSON Pointer locations. +JSON and YAML mappings reject duplicate keys. Parsers reject alias cycles, +non-finite numbers, excessive nesting, and documents that exceed configured +limits. + +The main limits are: + +```julia +OpenAPI.normalize( + source; + base_uri = nothing, # identity for inline JSON or YAML + max_bytes = 16 * 1024 * 1024, + max_nodes = 1_000_000, + max_depth = 512, + max_resources = 256, + max_diagnostics = 1_000, +) +``` + +## References + +OpenAPI.jl resolves reusable OpenAPI objects and JSON Schema references. It +uses the isolated `OpenAPI.SchemaEngine` module for resource identity, URI +resolution, JSON Pointer, anchors, schema dialects, schema compilation, and +runtime validation. + +The schema engine is provisional. It is kept under `src/schema_engine` with no +OpenAPI-specific behavior so it can move to JSONSchema.jl after the API and +implementation have hardened against real OpenAPI documents. + +The default retriever has conservative access rules: + +- A local root can read relative files under the root file's directory. +- Extra local roots require `file_roots=[...]`. +- A URL root can read same-origin HTTP or HTTPS references. +- Cross-origin references require `allow_remote_refs=true`. +- HTTP redirects are not followed. +- Unsupported URI schemes are rejected. + +Pass an `OpenAPI.SchemaEngine.Resources.AbstractRetriever` with `retriever=...` +when an application needs another retrieval policy or an in-memory resource +store. Resource size and count limits still apply. + +Non-schema reference cycles are rejected. Recursive JSON Schemas are retained +and compiled normally. OpenAPI 3.0 Reference Object siblings are ignored. +OpenAPI 3.1 and 3.2 `summary` and `description` siblings are applied. Path Item +Reference Object siblings have undefined specification behavior. Strict mode +rejects them. Permissive mode warns and lets local fields override the target. + +## Generated model behavior + +Generated structs are a typed view over the document's JSON Schemas. Runtime +schema validation remains authoritative. This design protects correctness when +a Julia field type cannot express every schema rule. + +Implemented model behavior includes: + +- objects, arrays, tuples, dictionaries, primitives, enums, and nullable types; +- required, optional, and explicit-null values; +- `allOf`, `oneOf`, `anyOf`, and discriminators; +- recursive models and recursive aliases; +- `additionalProperties`, `patternProperties`, `propertyNames`, and closed + objects; +- JSON Schema assertions such as `const`, `not`, conditions, dependent rules, + bounds, formats, and unevaluated constraints through runtime validation; +- `readOnly` and `writeOnly` request and response projections; +- Julia `Date`, `Time`, `DateTime`, `UUID`, and base64 byte values; +- deterministic names with protection against Julia keywords, Base/Core names, + and generated runtime names. + +An unusual schema can plan to `Any` when no useful Julia type exists. It is +still validated at request and response boundaries. Custom JSON Schema dialects +and custom vocabularies can therefore retain correct validation while using a +less precise Julia type. + +Set `validate_requests=false` or `validate_responses=false` on a generated +`Client` only when the application accepts that loss of boundary validation. +For example, a response schema with `additionalProperties: false` rejects a new +server field. This is contract-correct but can make a client less tolerant of +an API that changes outside its published contract. + +## HTTP behavior + +Generated clients support: + +- path, query, header, and cookie parameters; +- `simple`, `label`, `matrix`, `form`, `spaceDelimited`, `pipeDelimited`, and + `deepObject` serialization where the specification permits each style; +- `allowReserved`, `allowEmptyValue`, explode defaults, and parameter `content`; +- JSON and structured-suffix JSON media types; +- text and binary bodies; +- `application/x-www-form-urlencoded` bodies; +- multipart bodies, per-property encodings, documented part headers, uploads, + and one required level of nested named OAS 3.2 encoding; +- JSON Lines, NDJSON, JSON text sequences, and GeoJSON text sequences when the + body is described by a normal schema; +- exact, wildcard, and structured-suffix media negotiation; +- exact response codes, `1XX` through `5XX` ranges, and `default` responses; +- documented response headers, including repeated headers and `Set-Cookie`; +- operation, path, and root servers, relative server URLs, named servers, and + validated server variables; +- request and response validation with input/output JSON Schema semantics. + +Use `content_type=...` and `accept=...` on an operation when the document offers +more than one representation. Use `request_headers` for one call and +`Client(headers=...)` for all calls. `request_options` passes options to +`HTTP.request`. + +For a custom media type, register an encoder or decoder: + +```julia +ExampleClient.codec!( + client, + "application/cbor"; + encode = (value, media_type) -> encode_cbor(value), + decode = (bytes, media_type) -> decode_cbor(bytes), +) +``` + +XML metadata is retained in the schema but does not generate an XML codec. +Register a custom codec for XML or another non-built-in representation. + +## Security + +Generated clients implement OpenAPI security requirement alternatives and +combinations. Supported credentials include: + +- API keys in headers, query parameters, or cookies; +- HTTP Basic and Bearer authentication; +- other HTTP authentication values; +- OAuth 2.0 and OpenID Connect bearer tokens with documented scope checks; +- mutual TLS through HTTP request options. + +```julia +ExampleClient.credential!( + client, + "bearerAuth", + ExampleClient.BearerCredential("token"; scopes = ["widgets:read"]), +) +``` + +`authorization!(client, token)` is a convenience for every bearer-compatible +scheme in a document. The generated client does not acquire or refresh OAuth or +OpenID Connect tokens. The caller owns that lifecycle. + +By default, a secured operation fails before network access when no documented +credential alternative can be satisfied. Set `require_credentials=false` only +when an external HTTP layer supplies authentication. + +## Support boundary + +The loader and normalizer preserve more OpenAPI information than an outgoing +client needs. The following boundaries are intentional and explicit: + +| Feature | Status | +| --- | --- | +| OAS 3.0.x, 3.1.x, and 3.2.x document loading | Supported | +| JSON and YAML, with duplicate-key rejection | Supported | +| Local, same-origin, opt-in remote, anchor, and recursive references | Supported | +| Standard operations, OAS 3.2 `QUERY`, and `additionalOperations` | Supported | +| Callback and webhook operations | Normalized and validated; no outgoing client functions are emitted | +| Link Objects | Preserved; no automatic follow-up operation is emitted | +| XML Object mapping | Preserved as schema metadata; use a custom media codec | +| OAS 3.2 `querystring` parameters | **Deferred. Client planning fails with `unsupported_querystring_generation`.** | +| OAS 3.2 streaming `itemSchema`, `itemEncoding`, and `prefixEncoding` | **Deferred. Client planning fails with `unsupported_streaming_generation`.** | + +The two deferred features fail during planning. They never produce a client +that silently sends the wrong wire format. + +`strict=true` is the default. Use `strict=false` only for documented ecosystem +compatibility cases. Permissive mode can retain ambiguous path templates and a +non-object `deepObject` parameter with warnings. For OAS 3.0 documents, it also +supports the common non-standard `nullable: true` plus `$ref` or `allOf` idiom. +Strict mode follows the normative rule that `nullable` only takes effect when +the same Schema Object defines `type`. Permissive mode does not suppress unsafe +or unsupported behavior. + +## Create a document from Julia declarations + +The authoring API is intentionally smaller than the ingestion and client +pipeline. It maps common Julia endpoint declarations to an OpenAPI 3.2.0 +document. + +```julia +using OpenAPI, JSON + +struct Widget + id::Int + tags::Vector{String} +end + +operations = [ + OpenAPI.Operation( + id = "get_widget", + method = :GET, + path = "/v1/widgets/{id}", + params = [ + OpenAPI.Param("id", :path, Int), + OpenAPI.Param("verbose", :query, Bool; required = false), + ], + responsetype = Widget, + ), +] + +document = OpenAPI.document( + operations; + title = "Widgets", + version = "1.0.0", +) + +write("openapi.json", JSON.json(document; pretty = 2)) +``` + +OpenAPI.jl does not depend on a server framework. Framework packages can add +optional `operations` and `register!` methods. Servo.jl provides its OpenAPI +adapter from a downstream package extension. + +## Validation evidence + +The test suite includes structural schemas published by the OpenAPI Initiative, +adversarial JSON and YAML parsing, external and cyclic references, OAS 3.0/3.1/ +3.2 semantics, JSON Schema edge cases, all parameter locations and styles, +security alternatives, server selection, media negotiation, nested multipart +encoding, error responses, and a live local HTTP integration server. + +An optional pinned corpus test generates and compiles clients from public +Petstore, Discord, Stripe, and GitHub descriptions. Run it with: + +```sh +OPENAPI_CORPUS_TESTS=small julia --project=. -e 'using Pkg; Pkg.test()' +OPENAPI_CORPUS_TESTS=all julia --project=. -e 'using Pkg; Pkg.test()' +OPENAPI_CORPUS_TESTS=all OPENAPI_CORPUS_CASE=GitHub julia --project=. -e 'using Pkg; Pkg.test()' +``` + +The large Stripe and GitHub descriptions require permissive mode for known +description-level compatibility warnings. Corpus success proves that a client +is generated and compiled. It does not prove that every operation was exercised +against each live service. + +Large descriptions also produce large generated modules because the client +keeps the schema data needed for runtime validation. The pinned Stripe and +GitHub cases are scaling gates for this design. Generation is practical, but +loading either client can take tens of seconds. Applications should generate +and precompile these clients during a build step, not at service startup. + +## Specification sources + +OpenAPI behavior follows the normative +[OpenAPI 3.0.4](https://spec.openapis.org/oas/v3.0.4.html), +[OpenAPI 3.1.1](https://spec.openapis.org/oas/v3.1.1.html), and +[OpenAPI 3.2.0](https://spec.openapis.org/oas/v3.2.0.html) specifications. +The files in `schemas/` are official structural schemas. The normative text +remains authoritative when a published schema differs from it. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..4c3700c --- /dev/null +++ b/SKILL.md @@ -0,0 +1,129 @@ +# Using OpenAPI.jl + +Use this guide when a task needs to inspect an OpenAPI description, generate a +Julia client, or create a basic OpenAPI document from Julia declarations. + +## Read and inspect a description + +```julia +using OpenAPI + +source = OpenAPI.load("openapi.yaml") +println(source.version) + +diagnostics = OpenAPI.check("openapi.yaml") +foreach(println, diagnostics) + +api = OpenAPI.normalize(source) +println(api.title) +println(length(api.operations)) +``` + +Use `using HTTP` before loading an HTTP or HTTPS URL. + +## Generate a Julia client + +```julia +using OpenAPI, HTTP + +OpenAPI.client( + "https://example.com/openapi.json"; + name = "ExampleClient", + path = "ExampleClient.jl", +) +``` + +The target environment for `ExampleClient.jl` must contain OpenAPI.jl, HTTP.jl, +and JSON.jl. + +```julia +include("ExampleClient.jl") + +client = ExampleClient.Client("https://api.example.com") +ExampleClient.authorization!(client, ENV["EXAMPLE_TOKEN"]) +result = ExampleClient.list_widgets(; client) +``` + +Use `ExampleClient.ABSENT` for an omitted optional value. Use `nothing` only +for an explicit nullable value. Pass `with_http_info=true` when status and +headers are needed. + +## External references + +Local roots may read relative references from their directory. Add explicit +roots when needed: + +```julia +api = OpenAPI.normalize( + "specs/root.yaml"; + file_roots = ["schemas", "shared-specs"], +) +``` + +A URL root permits same-origin references. Use `allow_remote_refs=true` only +when the document is trusted to select cross-origin resources. For a controlled +resource store, pass an `OpenAPI.SchemaEngine.Resources.AbstractRetriever` as +`retriever`. + +## Strict and permissive modes + +Keep `strict=true` for normal work. If a public description uses a known +ecosystem extension, inspect the diagnostics before using permissive mode: + +```julia +api = OpenAPI.normalize(source; strict = false) +foreach(println, api.diagnostics) +``` + +Permissive mode does not bypass unsupported feature checks. Client generation +still fails for OAS 3.2 `querystring` parameters and streaming or positional +`itemSchema`, `itemEncoding`, and `prefixEncoding` behavior. + +## Custom body codecs + +```julia +ExampleClient.codec!( + client, + "application/cbor"; + encode = (value, media_type) -> encode_cbor(value), + decode = (bytes, media_type) -> decode_cbor(bytes), +) +``` + +Use this mechanism for XML and other representations that do not have a +built-in codec. + +## Create a document + +```julia +using OpenAPI, JSON + +operations = [ + OpenAPI.Operation( + id = "get_item", + method = :GET, + path = "/items/{id}", + params = [OpenAPI.Param("id", :path, Int)], + responsetype = Item, + ), +] + +document = OpenAPI.document(operations; title = "Items", version = "1.0.0") +println(JSON.json(document; pretty = 2)) +``` + +This authoring API covers common Julia endpoint declarations. It is not a +general builder for every OpenAPI Object. Build advanced documents as JSON-like +objects and pass them through `OpenAPI.validate` or `OpenAPI.normalize`. + +## Verify generated output + +At minimum: + +1. Include the generated source in a fresh module. +2. Exercise a real or local HTTP endpoint. +3. Inspect serialized path, query, header, cookie, and body values. +4. Test a documented success and a documented error response. +5. Keep request and response schema validation enabled. + +See `README.md` for the detailed feature and support matrix. diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 2cb1385..0000000 --- a/codecov.yml +++ /dev/null @@ -1,2 +0,0 @@ -ignore: - - "src/tools.jl" \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 378eac2..0000000 --- a/docs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build diff --git a/docs/Project.toml b/docs/Project.toml deleted file mode 100644 index fb315d1..0000000 --- a/docs/Project.toml +++ /dev/null @@ -1,5 +0,0 @@ -[deps] -Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" - -[compat] -Documenter = "0.27" \ No newline at end of file diff --git a/docs/make.jl b/docs/make.jl deleted file mode 100644 index 8f38a1f..0000000 --- a/docs/make.jl +++ /dev/null @@ -1,24 +0,0 @@ -import Pkg -Pkg.add("Documenter") - -using Documenter -using OpenAPI - -makedocs( - sitename = "OpenAPI.jl", - format = Documenter.HTML( - prettyurls = get(ENV, "CI", nothing) == "true" - ), - pages = [ - "Home" => "index.md", - "User Guide" => "userguide.md", - "Reference" => "reference.md", - "Tools" => "tools.md", - "TODO" => "todo.md", - ], -) - -deploydocs( - repo = "github.com/JuliaComputing/OpenAPI.jl.git", - push_preview = true, -) \ No newline at end of file diff --git a/docs/src/index.md b/docs/src/index.md deleted file mode 100644 index f2f28b6..0000000 --- a/docs/src/index.md +++ /dev/null @@ -1,12 +0,0 @@ -# OpenAPI.jl - -This is the Julia library needed along with code generated by the [OpenAPI generator](https://openapi-generator.tech/) to help define, produce and consume OpenAPI interfaces. - -The goal of OpenAPI is to define a standard, language-agnostic interface to REST APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection. When properly defined via OpenAPI, a consumer can understand and interact with the remote service with a minimal amount of implementation logic. Similar to what interfaces have done for lower-level programming, OpenAPI removes the guesswork in calling the service. - -Check out [OpenAPI-Spec](https://github.com/OAI/OpenAPI-Specification) for additional information about the OpenAPI project, including additional libraries with support for other languages and more. - -## Migrating from Swagger.jl - -This package supersedes the [Swagger.jl](https://github.com/JuliaComputing/Swagger.jl) package. OpenAPI.jl and the associated generator can address both OpenAPI 2.x (Swagger) and OpenAPI 3.x specifications. Code dependent on Swagger.jl would not directly work with OpenAPI.jl, but migration should not be too difficult. - diff --git a/docs/src/reference.md b/docs/src/reference.md deleted file mode 100644 index f2c6449..0000000 --- a/docs/src/reference.md +++ /dev/null @@ -1,68 +0,0 @@ -```@contents -Pages = ["reference.md"] -Depth = 3 -``` - -```@meta -CurrentModule = OpenAPI -``` - -# API Reference - -## Client - -```@docs -Clients.Client -Clients.set_user_agent -Clients.set_cookie -Clients.set_header -Clients.set_timeout -``` - -## Examining Models - -```@docs -hasproperty -getproperty -setproperty! -Clients.getpropertyat -Clients.haspropertyat -``` - -## Examining Client API Response - -```@docs -Clients.ApiResponse -``` - -```@docs -Clients.is_longpoll_timeout -``` - -```@docs -Clients.is_request_interrupted -``` - -```@docs -Clients.storefile -``` - -## Server - -The server code is generated as a package. It contains API stubs and validations of API inputs. It requires the caller to -have implemented the APIs, the signatures of which are provided in the generated package module docstring. - -Refer to the User Guide section for mode details of the API that is generated. - -## Tools - -```@docs -openapi_generator -stop_openapi_generator -generate -swagger_ui -stop_swagger_ui -swagger_editor -stop_swagger_editor -lint -``` \ No newline at end of file diff --git a/docs/src/todo.md b/docs/src/todo.md deleted file mode 100644 index 25b3581..0000000 --- a/docs/src/todo.md +++ /dev/null @@ -1,14 +0,0 @@ -# TODO - -Not all OpenAPI features are supported yet, e.g.: -- [`not`](https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/) -- [inheritance and polymorphism](https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/) -- [some of the JSON schema keywords](https://swagger.io/docs/specification/data-models/keywords/) -- some [subtler data types](https://swagger.io/docs/specification/data-models/data-types/) - - native representaion of some of the string formats, e.g. uuid, url - - read-only and write-only properties -- better enum support -- authentication schemes -- [`deepObject`](https://swagger.io/docs/specification/serialization/)s in query parameters - -There could be more unsupported features than what is listed above. diff --git a/docs/src/tools.md b/docs/src/tools.md deleted file mode 100644 index f44ed81..0000000 --- a/docs/src/tools.md +++ /dev/null @@ -1,146 +0,0 @@ -# Tools - -## Code Generator - -The [OpenAPI Generator Docker image](https://hub.docker.com/r/openapitools/openapi-generator-cli) is a code generator that can generate client libraries, server stubs, and API documentation from an OpenAPI Specification. OpenAPI.jl includes convenience methods to use the OpenAPI Generator from Julia. - -Use `OpenAPI.generate` to generate code from an OpenAPI specification. It can be pointed at a server hosted on the local machine or a remote server. The OpenAPI Generator must be running at the specified `generator_host`. Returns the folder containing generated code. - -```julia -OpenAPI.generate( - spec::Dict{String,Any}; - type::Symbol=:client, - package_name::AbstractString="APIClient", - export_models::Bool=false, - export_operations::Bool=false, - output_dir::AbstractString="", - generator_host::AbstractString=GeneratorHost.Local -) -``` - -Arguments: -- `spec`: The OpenAPI specification as a Dict. It can be obtained by parsing a JSON or YAML file using `JSON.parse` or `YAML.load`. - -Optional arguments: -- `type`: The type of code to generate. Must be `:client` or `:server`. Defaults to `:client`. -- `package_name`: The name of the package to generate. Defaults to "APIClient". -- `export_models`: Whether to export models. Defaults to false. -- `export_operations`: Whether to export operations. Defaults to false. -- `output_dir`: The directory to save the generated code. Defaults to a temporary directory. Directory will be created if it does not exist. -- `generator_host`: The host of the OpenAPI Generator. Defaults to `GeneratorHost.Local` (which points to `http://localhost:8080`). - -The `generator_host` can be pointed to any other URL where the OpenAPI Generator is running, e.g. `https://openapigen.myorg.com`. Other possible pre-defined values of `generator_host`, which point to the public service hosted by OpenAPI org are: -- `OpenAPI.GeneratorHost.OpenAPIGeneratorTech.Stable`: Runs a stable version of the OpenAPI Generator at . -- `OpenAPI.GeneratorHost.OpenAPIGeneratorTech.Master`: Runs the latest version of the OpenAPI Generator at . - -A locally hosted generator service is preferred by default for privacy reasons. One can be started on the local machine using `OpenAPI.openapi_generator`. It uses the `openapitools/openapi-generator-online` docker image and requires docker engine to be installed. Use `OpenAPI.stop_openapi_generator` to stop the local generator service after use. - -```julia -OpenAPI.openapi_generator(; - port::Int=8080, # port to use - use_sudo::Bool=false # whether to use sudo while invoking docker -) - -OpenAPI.stop_openapi_generator(; - use_sudo::Bool=false # whether to use sudo while invoking docker -) -``` - -## Swagger UI - -[Swagger UI](https://swagger.io/tools/swagger-ui/) allows visualization and interaction with the API’s resources without having any of the implementation logic in place. OpenAPI.jl includes convenience methods to launch Swagger UI from Julia. - -Use `OpenAPI.swagger_ui` to open Swagger UI. It uses the standard `swaggerapi/swagger-ui` docker image and requires docker engine to be installed. - -```julia -# provide a specification file to start with -OpenAPI.swagger_ui( - spec::AbstractString; # the OpenAPI specification to use - port::Int=8080, # port to use - use_sudo::Bool=false # whether to use sudo while invoking docker -) - -# provide a folder and specification file name to start with -OpenAPI.swagger_ui( - spec_dir::AbstractString; # folder containing the specification file - spec_file::AbstractString; # the specification file - port::Int=8080, # port to use - use_sudo::Bool=false # whether to use sudo while invoking docker -) -``` - -It returns the URL that should be opened in a browser to access the Swagger UI. Combining it with a tool like [DefaultApplication.jl](https://github.com/tpapp/DefaultApplication.jl) can help open a browser tab directly from Julia. - -```julia -DefaultApplication.open(OpenAPI.swagger_ui("/my/openapi/spec.json")) -``` - -To stop the Swagger UI container, use `OpenAPI.stop_swagger_ui`. - -```julia -OpenAPI.stop_swagger_ui(; - use_sudo::Bool=false # whether to use sudo while invoking docker -) -``` - -## Swagger Editor - -[Swagger Editor](https://swagger.io/tools/swagger-editor/) allows editing of OpenAPI specifications and simultaneous visualization and interaction with the API’s resources without having any of the client implementation logic in place. OpenAPI.jl includes convenience methods to launch Swagger Editor from Julia. - -Use `OpenAPI.swagger_editor` to open Swagger Editor. It uses the standard `swaggerapi/swagger-editor` docker image and requires docker engine to be installed. - -```julia -# specify a specification file to start with -OpenAPI.swagger_editor( - spec::AbstractString; # the OpenAPI specification to use - port::Int=8080, # port to use - use_sudo::Bool=false # whether to use sudo while invoking docker -) - -# specify a folder and specification file name to start with -OpenAPI.swagger_editor( - spec_dir::AbstractString; # folder containing the specification file - spec_file::AbstractString; # the specification file - port::Int=8080, # port to use - use_sudo::Bool=false # whether to use sudo while invoking docker -) - -# start without specifying any initial specification file -OpenAPI.swagger_editor( - port::Int=8080, # port to use - use_sudo::Bool=false # whether to use sudo while invoking docker -) -``` - -It returns the URL that should be opened in a browser to access the Swagger UI. Combining it with a tool like [DefaultApplication.jl](https://github.com/tpapp/DefaultApplication.jl) can help open a browser tab directly from Julia. - -```julia -DefaultApplication.open(OpenAPI.swagger_editor("/my/openapi/spec.json")) -``` - -To stop the Swagger Editor container, use `OpenAPI.stop_swagger_editor`. - -```julia -OpenAPI.stop_swagger_editor(; - use_sudo::Bool=false # whether to use sudo while invoking docker -) -``` - -## Spectral Linter - -[Spectral](https://stoplight.io/open-source/spectral) is an open-source API style guide enforcer and linter. OpenAPI.jl includes a convenience method to use the Spectral OpenAPI linter from Julia. - -```julia -# specify a specification file to start with -OpenAPI.lint( - spec::AbstractString; # the OpenAPI specification to use - use_sudo::Bool=false # whether to use sudo while invoking docker -) - -# specify a folder and specification file name to start with -OpenAPI.lint( - spec_dir::AbstractString; # folder containing the specification file - spec_file::AbstractString; # the specification file - use_sudo::Bool=false # whether to use sudo while invoking docker -) -``` diff --git a/docs/src/userguide.md b/docs/src/userguide.md deleted file mode 100644 index 245e7a4..0000000 --- a/docs/src/userguide.md +++ /dev/null @@ -1,231 +0,0 @@ -# User Guide - -## Code Generation - -Use [instructions](https://openapi-generator.tech/docs/generators) provided for the Julia OpenAPI code generator plugin to generate Julia code. - -Requires version [6.3.0](https://github.com/OpenAPITools/openapi-generator/releases/tag/v6.3.0) or later of [openapi-generator](https://github.com/OpenAPITools/openapi-generator). - -## Models - -Each model from the specification is generated into a file named `model_.jl`. It is represented as a `mutable struct` that is a subtype of the abstract type `APIModel`. Models have the following methods defined: - -- constructor that takes keyword arguments to fill in values for all model properties. -- [`propertynames`](https://docs.julialang.org/en/v1/base/base/#Base.propertynames) -- [`hasproperty`](https://docs.julialang.org/en/v1/base/base/#Base.hasproperty) -- [`getproperty`](https://docs.julialang.org/en/v1/base/base/#Base.getproperty) -- [`setproperty!`](https://docs.julialang.org/en/v1/base/base/#Base.setproperty!) - -In addition to these standard Julia methods, these convenience methods are also generated that help in checking value at a hierarchical path of the model. - -- `function haspropertyat(o::T, path...) where {T<:APIModel}` -- `function getpropertyat(o::T, path...) where {T<:APIModel}` - -E.g: - -```julia -# access o.field.subfield1.subfield2 -if haspropertyat(o, "field", "subfield1", "subfield2") - getpropertyat(o, "field", "subfield1", "subfield2") -end - -# access nested array elements, e.g. o.field2.subfield1[10].subfield2 -if haspropertyat(o, "field", "subfield1", 10, "subfield2") - getpropertyat(o, "field", "subfield1", 10, "subfield2") -end -``` - -## Validations - -Following validations are incorporated into models: - -- maximum value: must be a numeric value less than or equal to a specified value -- minimum value: must be a numeric value greater than or equal to a specified value -- maximum length: must be a string value of length less than or equal to a specified value -- minimum length: must be a string value of length greater than or equal to a specified value -- maximum item count: must be a list value with number of items less than or equal to a specified value -- minimum item count: must be a list value with number of items greater than or equal to a specified value -- unique items: items must be unique -- maximum properties count: number of properties must be less than or equal to a specified value -- minimum properties count: number of properties must be greater than or equal to a specified value -- pattern: must match the specified regex pattern -- format: must match the specified format specifier (see subsection below for details) -- enum: value must be from a list of allowed values -- multiple of: must be a multiple of a specified value - -Validations are imposed in the constructor and `setproperty!` methods of models. - -#### Validations for format specifiers - -String, number and integer data types can have an optional format modifier that serves as a hint at the contents and format of the string. Validations for the following OpenAPI defined formats are built in: - -| Data Type | Format | Description | -|-----------|-----------|-------------| -| number | float | Floating-point numbers. | -| number | double | Floating-point numbers with double precision. | -| integer | int32 | Signed 32-bit integers (commonly used integer type). | -| integer | int64 | Signed 64-bit integers (long type). | -| string | date | full-date notation as defined by RFC 3339, section 5.6, for example, 2017-07-21 | -| string | date-time | the date-time notation as defined by RFC 3339, section 5.6, for example, 2017-07-21T17:32:28Z | -| string | byte | base64-encoded characters, for example, U3dhZ2dlciByb2Nrcw== | - -Validations for custom formats can be plugged in by overloading the `OpenAPI.val_format` method. - -E.g.: - -```julia -# add a new validation named `custom` for the number type -function OpenAPI.val_format(val::AbstractFloat, ::Val{:custom}) - return true # do some validations and return result -end -# add a new validation named `custom` for the integer type -function OpenAPI.val_format(val::Integer, ::Val{:custom}) - return true # do some validations and return result -end -# add a new validation named `custom` for the string type -function OpenAPI.val_format(val::AbstractString, ::Val{:custom}) - return true # do some validations and return result -end -``` - -## Client APIs - -Each client API set is generated into a file named `api_.jl`. It is represented as a `struct` and the APIs under it are generated as methods. An API set can be constructed by providing the OpenAPI client instance that it can use for communication. - -The required API parameters are generated as regular function arguments. Optional parameters are generated as keyword arguments. Method documentation is generated with description, parameter information and return value. Two variants of the API are generated. The first variant is suitable for calling synchronously. It returns a tuple of the result struct and the HTTP response. - -```julia -# example synchronous API that returns an Order instance -getOrderById(api::StoreApi, orderId::Int64) -> (result, http_response) -``` - -The second variant is suitable for asynchronous calls to methods that return chunked transfer encoded responses, where in the API streams the response objects into an output channel. - -```julia -# example asynchronous API that streams matching Pet instances into response_stream -findPetsByStatus( - api::PetApi, - response_stream::Channel, - status::Vector{String}) -> (response_stream, http_response) -``` - -The HTTP response returned from the API calls, have these properties: -- `status`: integer status code -- `message`: http message corresponding to status code -- `headers`: http response headers as `Vector{Pair{String,String}}` - -A client context holds common information to be used across APIs. It also holds a connection to the server and uses that across API calls. -The client context needs to be passed as the first parameter of all API calls. It can be created as: - -```julia -Client(root::String; - headers::Dict{String,String}=Dict{String,String}(), - get_return_type::Function=(default,data)->default, - timeout::Int=DEFAULT_TIMEOUT_SECS, - long_polling_timeout::Int=DEFAULT_LONGPOLL_TIMEOUT_SECS, - pre_request_hook::Function, - escape_path_params::Union{Nothing,Bool}=nothing, - chunk_reader_type::Union{Nothing,Type{<:AbstractChunkReader}}=nothing, - verbose::Union{Bool,Function}=false, - httplib::Symbol=OpenAPI.HTTPLib.Downloads, -) -``` - -Where: - -- `root`: the root URI where APIs are hosted (should not end with a `/`) -- `headers`: any additional headers that need to be passed along with all API calls -- `get_return_type`: optional method that can map a Julia type to a return type other than what is specified in the API specification by looking at the data (this is used only in special cases, for example when models are allowed to be dynamically loaded) -- `timeout`: optional timeout to apply for server methods (default `OpenAPI.Clients.DEFAULT_TIMEOUT_SECS`) -- `long_polling_timeout`: optional timeout to apply for long polling methods (default `OpenAPI.Clients.DEFAULT_LONGPOLL_TIMEOUT_SECS`) -- `pre_request_hook`: user provided hook to modify the request before it is sent -- `escape_path_params`: Whether the path parameters should be escaped before being used in the URL (true by default). This is useful if the path parameters contain characters that are not allowed in URLs or contain path separators themselves. -- `chunk_reader_type`: The type of chunk reader to be used for streaming responses. -- `verbose`: whether to enable verbose logging (behavior depends on chosen HTTP backend) -- `httplib`: The HTTP client library to use for making requests. Can be `OpenAPI.HTTPLib.Downloads` (default) for Downloads.jl or `OpenAPI.HTTPLib.HTTP` for HTTP.jl. - -The `pre_request_hook` must provide the following two implementations: -- `pre_request_hook(ctx::OpenAPI.Clients.Ctx) -> ctx` -- `pre_request_hook(resource_path::AbstractString, body::Any, headers::Dict{String,String}) -> (resource_path, body, headers)` - -The `chunk_reader_type` can be one of `LineChunkReader`, `JSONChunkReader` or `RFC7464ChunkReader`. If not specified, then the type is automatically determined based on the return type of the API call. Refer to the [Streaming Responses](#Streaming-Responses) section for more details. - -The `verbose` option can be one of: -- `false`: the default, no verbose logging -- `true`: enables verbose logging to stderr -- a function that accepts two arguments - type and message **(only supported with Downloads.jl backend; available on Julia version >= 1.7)** - - a default implementation of this that uses `@info` to log the arguments is provided as `OpenAPI.Clients.default_debug_hook` - - **Note:** This option is not supported when using the HTTP.jl backend. With HTTP.jl, use `verbose=true` for boolean verbose logging only. - -In case of any errors an instance of `ApiException` is thrown. It has the following fields: - -- `status::Int`: HTTP status code -- `reason::String`: Optional human readable string -- `resp::Downloads.Response`: The HTTP Response for this call -- `error::Union{Nothing,Downloads.RequestError}`: The HTTP error on request failure - -An API call involves the following steps: -- If a pre request hook is provided, it is invoked with an instance of `OpenAPI.Clients.Ctx` that has the request attributes. The hook method is expected to make any modifications it needs to the request attributes before the request is prepared, and return the modified context. -- The URL to be invoked is prepared by replacing placeholders in the API URL template with the supplied function parameters. -- If this is a POST request, serialize the instance of `APIModel` provided as the `body` parameter as a JSON document. -- If a pre request hook is provided, it is invoked with the prepared resource path, body and request headers. The hook method is expected to modify and return back a tuple of resource path, body and headers which will be used to make the request. -- Make the HTTP call to the API endpoint and collect the response. -- Determine the response type / model, invoke the optional user specified mapping function if one was provided. -- Convert (deserialize) the response data into the return type and return. -- In case of any errors, throw an instance of `ApiException` - -## Server APIs - -The server code is generated as a package. It contains API stubs and validations of API inputs. It requires the caller to -have implemented the APIs, the signatures of which are provided in the generated package module docstring. - -A `register` function is made available that when provided with a `Router` instance, registers handlers -for all the APIs. - -`register(router, impl; path_prefix="", optional_middlewares...) -> HTTP.Router` - -Paramerets: -- `router`: `HTTP.Router` to register handlers in, the same instance is also returned -- `impl`: module that implements the server APIs - -Optional parameters: -- `path_prefix`: prefix to be applied to all paths -- `optional_middlewares`: Register one or more optional middlewares to be applied to all requests. - -Optional middlewares can be one or more of: -- `init`: called before the request is processed -- `pre_validation`: called after the request is parsed but before validation -- `pre_invoke`: called after validation but before the handler is invoked -- `post_invoke`: called after the handler is invoked but before the response is sent - -The order in which middlewares are invoked is: -`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` - -## Responses - -The server APIs can return the Julia type that is specified in the OpenAPI specification. The response is serialized as JSON and sent back to the client. The default HTTP response code used in this case is 200. - -To return a custom HTTP response code, the server API can return a `HTTP.Response` instance directly. The OpenAPI package provides a overridden constructor for `HTTP.Response` that takes the desired HTTP code and the Julia struct that needs to be serialized as JSON and sent back to the client. It also sets the `Content-Type` header to `application/json`. - -```julia -HTTP.Response(code::Integer, o::APIModel) -``` - -Structured error messages can also be returned in similar fashion. Any uncaught exception thrown by the server API is caught and converted into a `HTTP.Response` instance with the HTTP code set to 500 and the exception message as the response body. - -## Streaming Responses - -Some OpenAPI implementations implement streaming of responses by sending more than one items in the response, each of which is of the type declared as the return type in the specification. E.g. the [Twitter OpenAPI specification](https://api.twitter.com/2/openapi.json) that keeps sending tweets in JSON like this forever: - -```json -{"data":{"id":"1800000000000000000","text":"mmm i like a sandwich"},"matching_rules":[{"id":1800000000000000000,"tag":"\"sandwich\""}]} -{"data":{"id":"1800000000000000001","text":"lets have a sandwich"},"matching_rules":[{"id":1800000000000000001,"tag":"\"sandwich\""}]} -``` - -OpenAPI.jl handles such responses through "chunk readers" which are engaged only with the streaming API endpoints. There can be multiple implementations of chunk readers, each of which must be of type `AbstractChunkReader`. The following are the chunk readers provided, each with a different chunk detection strategy. They are selected based on some heuristics based on the response data type. - -- `LineChunkReader`: Chunks delimited by newline. This is the default when the response type is detected to be not of `OpenAPI.APIModel` type. -- `JSONChunkReader`: Each chunk is a JSON. Whitespaces between JSONs are ignored. This is the default when the response type is detected to be a `OpenAPI.APIModel`. -- `RFC7464ChunkReader`: A reader based on [RFC 7464](https://www.rfc-editor.org/rfc/rfc7464.html). Available for use by overriding through `Client` or `Ctx`. - -The `OpenAPI.Clients.Client` and `OpenAPI.Clients.Ctx` constructors take an additional `chunk_reader_type` keyword parameter. This can be one of `OpenAPI.Clients.LineChunkReader`, `OpenAPI.Clients.JSONChunkReader` or `OpenAPI.Clients.RFC7464ChunkReader`. If not specified, then the type is automatically determined as described above. diff --git a/ext/OpenAPIHTTPExt.jl b/ext/OpenAPIHTTPExt.jl new file mode 100644 index 0000000..9c87013 --- /dev/null +++ b/ext/OpenAPIHTTPExt.jl @@ -0,0 +1,60 @@ +# Lets OpenAPI.read fetch documents over HTTP (e.g. a running app's +# /openapi.json). Loads automatically when both OpenAPI and HTTP are loaded. +module OpenAPIHTTPExt + +using OpenAPI, HTTP + +function OpenAPI.fetchresource(id::OpenAPI.Resources.ResourceId, max_bytes::Integer) + body = Vector{UInt8}(undef, max_bytes) + response = try + HTTP.get( + string(id); + status_exception = false, + redirect = false, + retry = false, + response_stream = body, + max_decompressed_size = max_bytes, + request_timeout = 60, + read_idle_timeout = 15, + ) + catch error + message = sprint(showerror, error) + if occursin("response stream", message) || + occursin("DecompressionLimitError", message) + throw( + OpenAPI.Resources.RetrievalError( + id, + "HTTP response exceeds the $max_bytes-byte limit", + ), + ) + end + throw( + OpenAPI.Resources.RetrievalError( + id, + "HTTP request failed: $message", + ), + ) + end + 200 <= response.status < 300 || throw( + OpenAPI.Resources.RetrievalError( + id, + "HTTP request returned status $(response.status)", + ), + ) + bytes = copy(body) + media_type = HTTP.header(response, "Content-Type", "") + return OpenAPI.Resources.RetrievedResource( + id, + bytes; + media_type = isempty(media_type) ? nothing : media_type, + ) +end + +OpenAPI.fetchurl(url::AbstractString) = String( + getfield( + OpenAPI.fetchresource(OpenAPI.Resources.ResourceId(url), 16 * 1024 * 1024), + :bytes, + ), +) + +end # module diff --git a/schemas/README.md b/schemas/README.md new file mode 100644 index 0000000..de3907d --- /dev/null +++ b/schemas/README.md @@ -0,0 +1,14 @@ +# OpenAPI validation schemas + +These files are informational JSON Schemas published by the OpenAPI +Initiative. OpenAPI.jl uses them for the structural validation pass. The +normative OpenAPI Specification remains authoritative when it differs from a +schema. + +- `oas-3.0.json`: +- `oas-3.1.json`: +- `oas-3.2.json`: + +The schemas are distributed under the Apache License 2.0 as part of the +OpenAPI Specification project. See +. diff --git a/schemas/oas-3.0.json b/schemas/oas-3.0.json new file mode 100644 index 0000000..a40570a --- /dev/null +++ b/schemas/oas-3.0.json @@ -0,0 +1,1651 @@ +{ + "id": "https://spec.openapis.org/oas/3.0/schema/2024-10-18", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "The description of OpenAPI v3.0.x Documents", + "type": "object", + "required": [ + "openapi", + "info", + "paths" + ], + "properties": { + "openapi": { + "type": "string", + "pattern": "^3\\.0\\.\\d(-.+)?$" + }, + "info": { + "$ref": "#/definitions/Info" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/SecurityRequirement" + } + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/Tag" + }, + "uniqueItems": true + }, + "paths": { + "$ref": "#/definitions/Paths" + }, + "components": { + "$ref": "#/definitions/Components" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false, + "definitions": { + "Reference": { + "type": "object", + "required": [ + "$ref" + ], + "patternProperties": { + "^\\$ref$": { + "type": "string", + "format": "uri-reference" + } + } + }, + "Info": { + "type": "object", + "required": [ + "title", + "version" + ], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "termsOfService": { + "type": "string", + "format": "uri-reference" + }, + "contact": { + "$ref": "#/definitions/Contact" + }, + "license": { + "$ref": "#/definitions/License" + }, + "version": { + "type": "string" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Contact": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "License": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Server": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "variables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/ServerVariable" + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "ServerVariable": { + "type": "object", + "required": [ + "default" + ], + "properties": { + "enum": { + "type": "array", + "items": { + "type": "string" + } + }, + "default": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Components": { + "type": "object", + "properties": { + "schemas": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "responses": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Response" + } + ] + } + } + }, + "parameters": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Parameter" + } + ] + } + } + }, + "examples": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Example" + } + ] + } + } + }, + "requestBodies": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/RequestBody" + } + ] + } + } + }, + "headers": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Header" + } + ] + } + } + }, + "securitySchemes": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/SecurityScheme" + } + ] + } + } + }, + "links": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Link" + } + ] + } + } + }, + "callbacks": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Callback" + } + ] + } + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "multipleOf": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "maxLength": { + "type": "integer", + "minimum": 0 + }, + "minLength": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "pattern": { + "type": "string", + "format": "regex" + }, + "maxItems": { + "type": "integer", + "minimum": 0 + }, + "minItems": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { + "type": "integer", + "minimum": 0 + }, + "minProperties": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "required": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "enum": { + "type": "array", + "items": {}, + "minItems": 1, + "uniqueItems": false + }, + "type": { + "type": "string", + "enum": [ + "array", + "boolean", + "integer", + "number", + "object", + "string" + ] + }, + "not": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "allOf": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "oneOf": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "anyOf": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "properties": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + }, + { + "type": "boolean" + } + ], + "default": true + }, + "description": { + "type": "string" + }, + "format": { + "type": "string" + }, + "default": {}, + "nullable": { + "type": "boolean", + "default": false + }, + "discriminator": { + "$ref": "#/definitions/Discriminator" + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "example": {}, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "xml": { + "$ref": "#/definitions/XML" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Discriminator": { + "type": "object", + "required": [ + "propertyName" + ], + "properties": { + "propertyName": { + "type": "string" + }, + "mapping": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "XML": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string", + "format": "uri" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean", + "default": false + }, + "wrapped": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Response": { + "type": "object", + "required": [ + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Header" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + } + }, + "links": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Link" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "MediaType": { + "type": "object", + "properties": { + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "example": {}, + "examples": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Example" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "encoding": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Encoding" + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false, + "allOf": [ + { + "$ref": "#/definitions/ExampleXORExamples" + } + ] + }, + "Example": { + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "value": {}, + "externalValue": { + "type": "string", + "format": "uri-reference" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Header": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "required": { + "type": "boolean", + "default": false + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "allowEmptyValue": { + "type": "boolean", + "default": false + }, + "style": { + "type": "string", + "enum": [ + "simple" + ], + "default": "simple" + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + }, + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + }, + "minProperties": 1, + "maxProperties": 1 + }, + "example": {}, + "examples": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Example" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false, + "allOf": [ + { + "$ref": "#/definitions/ExampleXORExamples" + }, + { + "$ref": "#/definitions/SchemaXORContent" + } + ] + }, + "Paths": { + "type": "object", + "patternProperties": { + "^\\/": { + "$ref": "#/definitions/PathItem" + }, + "^x-": {} + }, + "additionalProperties": false + }, + "PathItem": { + "type": "object", + "properties": { + "$ref": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "get": { + "$ref": "#/definitions/Operation" + }, + "put": { + "$ref": "#/definitions/Operation" + }, + "post": { + "$ref": "#/definitions/Operation" + }, + "delete": { + "$ref": "#/definitions/Operation" + }, + "options": { + "$ref": "#/definitions/Operation" + }, + "head": { + "$ref": "#/definitions/Operation" + }, + "patch": { + "$ref": "#/definitions/Operation" + }, + "trace": { + "$ref": "#/definitions/Operation" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + }, + "parameters": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Parameter" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "uniqueItems": true + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Operation": { + "type": "object", + "required": [ + "responses" + ], + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "operationId": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Parameter" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "uniqueItems": true + }, + "requestBody": { + "oneOf": [ + { + "$ref": "#/definitions/RequestBody" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "responses": { + "$ref": "#/definitions/Responses" + }, + "callbacks": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Callback" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/SecurityRequirement" + } + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Responses": { + "type": "object", + "properties": { + "default": { + "oneOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "patternProperties": { + "^[1-5](?:\\d{2}|XX)$": { + "oneOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "^x-": {} + }, + "minProperties": 1, + "additionalProperties": false + }, + "SecurityRequirement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "Tag": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "ExternalDocumentation": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "ExampleXORExamples": { + "description": "Example and examples are mutually exclusive", + "not": { + "required": [ + "example", + "examples" + ] + } + }, + "SchemaXORContent": { + "description": "Schema and content are mutually exclusive, at least one is required", + "not": { + "required": [ + "schema", + "content" + ] + }, + "oneOf": [ + { + "required": [ + "schema" + ] + }, + { + "required": [ + "content" + ], + "description": "Some properties are not allowed if content is present", + "allOf": [ + { + "not": { + "required": [ + "style" + ] + } + }, + { + "not": { + "required": [ + "explode" + ] + } + }, + { + "not": { + "required": [ + "allowReserved" + ] + } + }, + { + "not": { + "required": [ + "example" + ] + } + }, + { + "not": { + "required": [ + "examples" + ] + } + } + ] + } + ] + }, + "Parameter": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "in": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean", + "default": false + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "allowEmptyValue": { + "type": "boolean", + "default": false + }, + "style": { + "type": "string" + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + }, + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + }, + "minProperties": 1, + "maxProperties": 1 + }, + "example": {}, + "examples": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Example" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false, + "required": [ + "name", + "in" + ], + "allOf": [ + { + "$ref": "#/definitions/ExampleXORExamples" + }, + { + "$ref": "#/definitions/SchemaXORContent" + } + ], + "oneOf": [ + { + "$ref": "#/definitions/PathParameter" + }, + { + "$ref": "#/definitions/QueryParameter" + }, + { + "$ref": "#/definitions/HeaderParameter" + }, + { + "$ref": "#/definitions/CookieParameter" + } + ] + }, + "PathParameter": { + "description": "Parameter in path", + "required": [ + "required" + ], + "properties": { + "in": { + "enum": [ + "path" + ] + }, + "style": { + "enum": [ + "matrix", + "label", + "simple" + ], + "default": "simple" + }, + "required": { + "enum": [ + true + ] + } + } + }, + "QueryParameter": { + "description": "Parameter in query", + "properties": { + "in": { + "enum": [ + "query" + ] + }, + "style": { + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ], + "default": "form" + } + } + }, + "HeaderParameter": { + "description": "Parameter in header", + "properties": { + "in": { + "enum": [ + "header" + ] + }, + "style": { + "enum": [ + "simple" + ], + "default": "simple" + } + } + }, + "CookieParameter": { + "description": "Parameter in cookie", + "properties": { + "in": { + "enum": [ + "cookie" + ] + }, + "style": { + "enum": [ + "form" + ], + "default": "form" + } + } + }, + "RequestBody": { + "type": "object", + "required": [ + "content" + ], + "properties": { + "description": { + "type": "string" + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + } + }, + "required": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "SecurityScheme": { + "oneOf": [ + { + "$ref": "#/definitions/APIKeySecurityScheme" + }, + { + "$ref": "#/definitions/HTTPSecurityScheme" + }, + { + "$ref": "#/definitions/OAuth2SecurityScheme" + }, + { + "$ref": "#/definitions/OpenIdConnectSecurityScheme" + } + ] + }, + "APIKeySecurityScheme": { + "type": "object", + "required": [ + "type", + "name", + "in" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "apiKey" + ] + }, + "name": { + "type": "string" + }, + "in": { + "type": "string", + "enum": [ + "header", + "query", + "cookie" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "HTTPSecurityScheme": { + "type": "object", + "required": [ + "scheme", + "type" + ], + "properties": { + "scheme": { + "type": "string" + }, + "bearerFormat": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "http" + ] + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false, + "oneOf": [ + { + "description": "Bearer", + "properties": { + "scheme": { + "type": "string", + "pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$" + } + } + }, + { + "description": "Non Bearer", + "not": { + "required": [ + "bearerFormat" + ] + }, + "properties": { + "scheme": { + "not": { + "type": "string", + "pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$" + } + } + } + } + ] + }, + "OAuth2SecurityScheme": { + "type": "object", + "required": [ + "type", + "flows" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flows": { + "$ref": "#/definitions/OAuthFlows" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "OpenIdConnectSecurityScheme": { + "type": "object", + "required": [ + "type", + "openIdConnectUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "openIdConnect" + ] + }, + "openIdConnectUrl": { + "type": "string", + "format": "uri-reference" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "OAuthFlows": { + "type": "object", + "properties": { + "implicit": { + "$ref": "#/definitions/ImplicitOAuthFlow" + }, + "password": { + "$ref": "#/definitions/PasswordOAuthFlow" + }, + "clientCredentials": { + "$ref": "#/definitions/ClientCredentialsFlow" + }, + "authorizationCode": { + "$ref": "#/definitions/AuthorizationCodeOAuthFlow" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "ImplicitOAuthFlow": { + "type": "object", + "required": [ + "authorizationUrl", + "scopes" + ], + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "PasswordOAuthFlow": { + "type": "object", + "required": [ + "tokenUrl", + "scopes" + ], + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "ClientCredentialsFlow": { + "type": "object", + "required": [ + "tokenUrl", + "scopes" + ], + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "AuthorizationCodeOAuthFlow": { + "type": "object", + "required": [ + "authorizationUrl", + "tokenUrl", + "scopes" + ], + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Link": { + "type": "object", + "properties": { + "operationId": { + "type": "string" + }, + "operationRef": { + "type": "string", + "format": "uri-reference" + }, + "parameters": { + "type": "object", + "additionalProperties": {} + }, + "requestBody": {}, + "description": { + "type": "string" + }, + "server": { + "$ref": "#/definitions/Server" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false, + "not": { + "description": "Operation Id and Operation Ref are mutually exclusive", + "required": [ + "operationId", + "operationRef" + ] + } + }, + "Callback": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/PathItem" + }, + "patternProperties": { + "^x-": {} + } + }, + "Encoding": { + "type": "object", + "properties": { + "contentType": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Header" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "style": { + "type": "string", + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + } + } +} diff --git a/schemas/oas-3.1.json b/schemas/oas-3.1.json new file mode 100644 index 0000000..54ddf62 --- /dev/null +++ b/schemas/oas-3.1.json @@ -0,0 +1,1411 @@ +{ + "$id": "https://spec.openapis.org/oas/3.1/schema/2025-09-15", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "The description of OpenAPI v3.1.x Documents without Schema Object validation", + "type": "object", + "properties": { + "openapi": { + "type": "string", + "pattern": "^3\\.1\\.\\d+(-.+)?$" + }, + "info": { + "$ref": "#/$defs/info" + }, + "jsonSchemaDialect": { + "type": "string", + "format": "uri-reference", + "default": "https://spec.openapis.org/oas/3.1/dialect/2024-11-10" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/server" + }, + "default": [ + { + "url": "/" + } + ] + }, + "paths": { + "$ref": "#/$defs/paths" + }, + "webhooks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/path-item" + } + }, + "components": { + "$ref": "#/$defs/components" + }, + "security": { + "type": "array", + "items": { + "$ref": "#/$defs/security-requirement" + } + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/$defs/tag" + } + }, + "externalDocs": { + "$ref": "#/$defs/external-documentation" + } + }, + "required": [ + "openapi", + "info" + ], + "anyOf": [ + { + "required": [ + "paths" + ] + }, + { + "required": [ + "components" + ] + }, + { + "required": [ + "webhooks" + ] + } + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false, + "$defs": { + "info": { + "$comment": "https://spec.openapis.org/oas/v3.1#info-object", + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "termsOfService": { + "type": "string", + "format": "uri-reference" + }, + "contact": { + "$ref": "#/$defs/contact" + }, + "license": { + "$ref": "#/$defs/license" + }, + "version": { + "type": "string" + } + }, + "required": [ + "title", + "version" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "contact": { + "$comment": "https://spec.openapis.org/oas/v3.1#contact-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "license": { + "$comment": "https://spec.openapis.org/oas/v3.1#license-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + }, + "required": [ + "name" + ], + "dependentSchemas": { + "identifier": { + "not": { + "required": [ + "url" + ] + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "server": { + "$comment": "https://spec.openapis.org/oas/v3.1#server-object", + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "variables": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/server-variable" + } + } + }, + "required": [ + "url" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "server-variable": { + "$comment": "https://spec.openapis.org/oas/v3.1#server-variable-object", + "type": "object", + "properties": { + "enum": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "default": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "default" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "components": { + "$comment": "https://spec.openapis.org/oas/v3.1#components-object", + "type": "object", + "properties": { + "schemas": { + "type": "object", + "additionalProperties": { + "$dynamicRef": "#meta" + } + }, + "responses": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/response-or-reference" + } + }, + "parameters": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/parameter-or-reference" + } + }, + "examples": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/example-or-reference" + } + }, + "requestBodies": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/request-body-or-reference" + } + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/header-or-reference" + } + }, + "securitySchemes": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/security-scheme-or-reference" + } + }, + "links": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/link-or-reference" + } + }, + "callbacks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/callbacks-or-reference" + } + }, + "pathItems": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/path-item" + } + } + }, + "patternProperties": { + "^(?:schemas|responses|parameters|examples|requestBodies|headers|securitySchemes|links|callbacks|pathItems)$": { + "$comment": "Enumerating all of the property names in the regex above is necessary for unevaluatedProperties to work as expected", + "propertyNames": { + "pattern": "^[a-zA-Z0-9._-]+$" + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "paths": { + "$comment": "https://spec.openapis.org/oas/v3.1#paths-object", + "type": "object", + "patternProperties": { + "^/": { + "$ref": "#/$defs/path-item" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "path-item": { + "$comment": "https://spec.openapis.org/oas/v3.1#path-item-object", + "type": "object", + "properties": { + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/server" + } + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/$defs/parameter-or-reference" + } + }, + "get": { + "$ref": "#/$defs/operation" + }, + "put": { + "$ref": "#/$defs/operation" + }, + "post": { + "$ref": "#/$defs/operation" + }, + "delete": { + "$ref": "#/$defs/operation" + }, + "options": { + "$ref": "#/$defs/operation" + }, + "head": { + "$ref": "#/$defs/operation" + }, + "patch": { + "$ref": "#/$defs/operation" + }, + "trace": { + "$ref": "#/$defs/operation" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "operation": { + "$comment": "https://spec.openapis.org/oas/v3.1#operation-object", + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/$defs/external-documentation" + }, + "operationId": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/$defs/parameter-or-reference" + } + }, + "requestBody": { + "$ref": "#/$defs/request-body-or-reference" + }, + "responses": { + "$ref": "#/$defs/responses" + }, + "callbacks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/callbacks-or-reference" + } + }, + "deprecated": { + "default": false, + "type": "boolean" + }, + "security": { + "type": "array", + "items": { + "$ref": "#/$defs/security-requirement" + } + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/server" + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "external-documentation": { + "$comment": "https://spec.openapis.org/oas/v3.1#external-documentation-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + }, + "required": [ + "url" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "parameter": { + "$comment": "https://spec.openapis.org/oas/v3.1#parameter-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "in": { + "enum": [ + "query", + "header", + "path", + "cookie" + ] + }, + "description": { + "type": "string" + }, + "required": { + "default": false, + "type": "boolean" + }, + "deprecated": { + "default": false, + "type": "boolean" + }, + "schema": { + "$dynamicRef": "#meta" + }, + "content": { + "$ref": "#/$defs/content", + "minProperties": 1, + "maxProperties": 1 + } + }, + "required": [ + "name", + "in" + ], + "oneOf": [ + { + "required": [ + "schema" + ] + }, + { + "required": [ + "content" + ] + } + ], + "if": { + "properties": { + "in": { + "const": "query" + } + } + }, + "then": { + "properties": { + "allowEmptyValue": { + "default": false, + "type": "boolean" + } + } + }, + "dependentSchemas": { + "schema": { + "properties": { + "style": { + "type": "string" + }, + "explode": { + "type": "boolean" + } + }, + "allOf": [ + { + "$ref": "#/$defs/examples" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-path" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-header" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-query" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-cookie" + }, + { + "$ref": "#/$defs/styles-for-form" + } + ], + "$defs": { + "styles-for-path": { + "if": { + "properties": { + "in": { + "const": "path" + } + } + }, + "then": { + "properties": { + "style": { + "default": "simple", + "enum": [ + "matrix", + "label", + "simple" + ] + }, + "required": { + "const": true + } + }, + "required": [ + "required" + ] + } + }, + "styles-for-header": { + "if": { + "properties": { + "in": { + "const": "header" + } + } + }, + "then": { + "properties": { + "style": { + "default": "simple", + "const": "simple" + } + } + } + }, + "styles-for-query": { + "if": { + "properties": { + "in": { + "const": "query" + } + } + }, + "then": { + "properties": { + "style": { + "default": "form", + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + }, + "allowReserved": { + "default": false, + "type": "boolean" + } + } + } + }, + "styles-for-cookie": { + "if": { + "properties": { + "in": { + "const": "cookie" + } + } + }, + "then": { + "properties": { + "style": { + "default": "form", + "const": "form" + } + } + } + } + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "parameter-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/parameter" + } + }, + "request-body": { + "$comment": "https://spec.openapis.org/oas/v3.1#request-body-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "content": { + "$ref": "#/$defs/content" + }, + "required": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "content" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "request-body-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/request-body" + } + }, + "content": { + "$comment": "https://spec.openapis.org/oas/v3.1#fixed-fields-10", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/media-type" + }, + "propertyNames": { + "format": "media-range" + } + }, + "media-type": { + "$comment": "https://spec.openapis.org/oas/v3.1#media-type-object", + "type": "object", + "properties": { + "schema": { + "$dynamicRef": "#meta" + }, + "encoding": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/encoding" + } + } + }, + "allOf": [ + { + "$ref": "#/$defs/specification-extensions" + }, + { + "$ref": "#/$defs/examples" + } + ], + "unevaluatedProperties": false + }, + "encoding": { + "$comment": "https://spec.openapis.org/oas/v3.1#encoding-object", + "type": "object", + "properties": { + "contentType": { + "type": "string", + "format": "media-range" + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/header-or-reference" + } + }, + "style": { + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean" + } + }, + "dependentSchemas": { + "style": { + "properties": { + "allowReserved": { + "default": false + } + } + }, + "explode": { + "properties": { + "style": { + "default": "form" + }, + "allowReserved": { + "default": false + } + } + }, + "allowReserved": { + "properties": { + "style": { + "default": "form" + } + } + } + }, + "allOf": [ + { + "$ref": "#/$defs/specification-extensions" + }, + { + "$ref": "#/$defs/styles-for-form" + } + ], + "unevaluatedProperties": false + }, + "responses": { + "$comment": "https://spec.openapis.org/oas/v3.1#responses-object", + "type": "object", + "properties": { + "default": { + "$ref": "#/$defs/response-or-reference" + } + }, + "patternProperties": { + "^[1-5](?:[0-9]{2}|XX)$": { + "$ref": "#/$defs/response-or-reference" + } + }, + "minProperties": 1, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false, + "if": { + "$comment": "either default, or at least one response code property must exist", + "patternProperties": { + "^[1-5](?:[0-9]{2}|XX)$": false + } + }, + "then": { + "required": [ + "default" + ] + } + }, + "response": { + "$comment": "https://spec.openapis.org/oas/v3.1#response-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/header-or-reference" + } + }, + "content": { + "$ref": "#/$defs/content" + }, + "links": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/link-or-reference" + } + } + }, + "required": [ + "description" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "response-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/response" + } + }, + "callbacks": { + "$comment": "https://spec.openapis.org/oas/v3.1#callback-object", + "type": "object", + "$ref": "#/$defs/specification-extensions", + "additionalProperties": { + "$ref": "#/$defs/path-item" + } + }, + "callbacks-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/callbacks" + } + }, + "example": { + "$comment": "https://spec.openapis.org/oas/v3.1#example-object", + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "value": true, + "externalValue": { + "type": "string", + "format": "uri-reference" + } + }, + "not": { + "required": [ + "value", + "externalValue" + ] + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "example-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/example" + } + }, + "link": { + "$comment": "https://spec.openapis.org/oas/v3.1#link-object", + "type": "object", + "properties": { + "operationRef": { + "type": "string", + "format": "uri-reference" + }, + "operationId": { + "type": "string" + }, + "parameters": { + "$ref": "#/$defs/map-of-strings" + }, + "requestBody": true, + "description": { + "type": "string" + }, + "server": { + "$ref": "#/$defs/server" + } + }, + "oneOf": [ + { + "required": [ + "operationRef" + ] + }, + { + "required": [ + "operationId" + ] + } + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "link-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/link" + } + }, + "header": { + "$comment": "https://spec.openapis.org/oas/v3.1#header-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "required": { + "default": false, + "type": "boolean" + }, + "deprecated": { + "default": false, + "type": "boolean" + }, + "schema": { + "$dynamicRef": "#meta" + }, + "content": { + "$ref": "#/$defs/content", + "minProperties": 1, + "maxProperties": 1 + } + }, + "oneOf": [ + { + "required": [ + "schema" + ] + }, + { + "required": [ + "content" + ] + } + ], + "dependentSchemas": { + "schema": { + "properties": { + "style": { + "default": "simple", + "const": "simple" + }, + "explode": { + "default": false, + "type": "boolean" + } + }, + "$ref": "#/$defs/examples" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "header-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/header" + } + }, + "tag": { + "$comment": "https://spec.openapis.org/oas/v3.1#tag-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/$defs/external-documentation" + } + }, + "required": [ + "name" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "reference": { + "$comment": "https://spec.openapis.org/oas/v3.1#reference-object", + "type": "object", + "properties": { + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "schema": { + "$comment": "https://spec.openapis.org/oas/v3.1#schema-object", + "$dynamicAnchor": "meta", + "type": [ + "object", + "boolean" + ] + }, + "security-scheme": { + "$comment": "https://spec.openapis.org/oas/v3.1#security-scheme-object", + "type": "object", + "properties": { + "type": { + "enum": [ + "apiKey", + "http", + "mutualTLS", + "oauth2", + "openIdConnect" + ] + }, + "description": { + "type": "string" + } + }, + "required": [ + "type" + ], + "allOf": [ + { + "$ref": "#/$defs/specification-extensions" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-apikey" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-http" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-http-bearer" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-oauth2" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-oidc" + } + ], + "unevaluatedProperties": false, + "$defs": { + "type-apikey": { + "if": { + "properties": { + "type": { + "const": "apiKey" + } + } + }, + "then": { + "properties": { + "name": { + "type": "string" + }, + "in": { + "enum": [ + "query", + "header", + "cookie" + ] + } + }, + "required": [ + "name", + "in" + ] + } + }, + "type-http": { + "if": { + "properties": { + "type": { + "const": "http" + } + } + }, + "then": { + "properties": { + "scheme": { + "type": "string" + } + }, + "required": [ + "scheme" + ] + } + }, + "type-http-bearer": { + "if": { + "properties": { + "type": { + "const": "http" + }, + "scheme": { + "type": "string", + "pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$" + } + }, + "required": [ + "type", + "scheme" + ] + }, + "then": { + "properties": { + "bearerFormat": { + "type": "string" + } + } + } + }, + "type-oauth2": { + "if": { + "properties": { + "type": { + "const": "oauth2" + } + } + }, + "then": { + "properties": { + "flows": { + "$ref": "#/$defs/oauth-flows" + } + }, + "required": [ + "flows" + ] + } + }, + "type-oidc": { + "if": { + "properties": { + "type": { + "const": "openIdConnect" + } + } + }, + "then": { + "properties": { + "openIdConnectUrl": { + "type": "string", + "format": "uri-reference" + } + }, + "required": [ + "openIdConnectUrl" + ] + } + } + } + }, + "security-scheme-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/security-scheme" + } + }, + "oauth-flows": { + "type": "object", + "properties": { + "implicit": { + "$ref": "#/$defs/oauth-flows/$defs/implicit" + }, + "password": { + "$ref": "#/$defs/oauth-flows/$defs/password" + }, + "clientCredentials": { + "$ref": "#/$defs/oauth-flows/$defs/client-credentials" + }, + "authorizationCode": { + "$ref": "#/$defs/oauth-flows/$defs/authorization-code" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false, + "$defs": { + "implicit": { + "type": "object", + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "authorizationUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "password": { + "type": "object", + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "client-credentials": { + "type": "object", + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "authorization-code": { + "type": "object", + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "authorizationUrl", + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + } + } + }, + "security-requirement": { + "$comment": "https://spec.openapis.org/oas/v3.1#security-requirement-object", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "specification-extensions": { + "$comment": "https://spec.openapis.org/oas/v3.1#specification-extensions", + "patternProperties": { + "^x-": true + } + }, + "examples": { + "properties": { + "example": true, + "examples": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/example-or-reference" + } + } + }, + "not": { + "required": [ + "example", + "examples" + ] + } + }, + "map-of-strings": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "styles-for-form": { + "if": { + "properties": { + "style": { + "const": "form" + } + }, + "required": [ + "style" + ] + }, + "then": { + "properties": { + "explode": { + "default": true + } + } + }, + "else": { + "properties": { + "explode": { + "default": false + } + } + } + } + } +} diff --git a/schemas/oas-3.2.json b/schemas/oas-3.2.json new file mode 100644 index 0000000..95ab03f --- /dev/null +++ b/schemas/oas-3.2.json @@ -0,0 +1,1684 @@ +{ + "$id": "https://spec.openapis.org/oas/3.2/schema/2025-11-23", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "The description of OpenAPI v3.2.x Documents without Schema Object validation", + "type": "object", + "properties": { + "openapi": { + "type": "string", + "pattern": "^3\\.2\\.\\d+(-.+)?$" + }, + "$self": { + "type": "string", + "format": "uri-reference", + "$comment": "MUST NOT contain a fragment", + "pattern": "^[^#]*$" + }, + "info": { + "$ref": "#/$defs/info" + }, + "jsonSchemaDialect": { + "type": "string", + "format": "uri-reference", + "default": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/server" + }, + "default": [ + { + "url": "/" + } + ] + }, + "paths": { + "$ref": "#/$defs/paths" + }, + "webhooks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/path-item" + } + }, + "components": { + "$ref": "#/$defs/components" + }, + "security": { + "type": "array", + "items": { + "$ref": "#/$defs/security-requirement" + } + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/$defs/tag" + } + }, + "externalDocs": { + "$ref": "#/$defs/external-documentation" + } + }, + "required": [ + "openapi", + "info" + ], + "anyOf": [ + { + "required": [ + "paths" + ] + }, + { + "required": [ + "components" + ] + }, + { + "required": [ + "webhooks" + ] + } + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false, + "$defs": { + "info": { + "$comment": "https://spec.openapis.org/oas/v3.2#info-object", + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "termsOfService": { + "type": "string", + "format": "uri-reference" + }, + "contact": { + "$ref": "#/$defs/contact" + }, + "license": { + "$ref": "#/$defs/license" + }, + "version": { + "type": "string" + } + }, + "required": [ + "title", + "version" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "contact": { + "$comment": "https://spec.openapis.org/oas/v3.2#contact-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "license": { + "$comment": "https://spec.openapis.org/oas/v3.2#license-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + }, + "required": [ + "name" + ], + "dependentSchemas": { + "identifier": { + "not": { + "required": [ + "url" + ] + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "server": { + "$comment": "https://spec.openapis.org/oas/v3.2#server-object", + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "variables": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/server-variable" + } + } + }, + "required": [ + "url" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "server-variable": { + "$comment": "https://spec.openapis.org/oas/v3.2#server-variable-object", + "type": "object", + "properties": { + "enum": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "default": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "default" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "components": { + "$comment": "https://spec.openapis.org/oas/v3.2#components-object", + "type": "object", + "properties": { + "schemas": { + "type": "object", + "additionalProperties": { + "$dynamicRef": "#meta" + } + }, + "responses": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/response-or-reference" + } + }, + "parameters": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/parameter-or-reference" + } + }, + "examples": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/example-or-reference" + } + }, + "requestBodies": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/request-body-or-reference" + } + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/header-or-reference" + } + }, + "securitySchemes": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/security-scheme-or-reference" + } + }, + "links": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/link-or-reference" + } + }, + "callbacks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/callbacks-or-reference" + } + }, + "pathItems": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/path-item" + } + }, + "mediaTypes": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/media-type-or-reference" + } + } + }, + "patternProperties": { + "^(?:schemas|responses|parameters|examples|requestBodies|headers|securitySchemes|links|callbacks|pathItems|mediaTypes)$": { + "$comment": "Enumerating all of the property names in the regex above is necessary for unevaluatedProperties to work as expected", + "propertyNames": { + "pattern": "^[a-zA-Z0-9._-]+$" + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "paths": { + "$comment": "https://spec.openapis.org/oas/v3.2#paths-object", + "type": "object", + "patternProperties": { + "^/": { + "$ref": "#/$defs/path-item" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "path-item": { + "$comment": "https://spec.openapis.org/oas/v3.2#path-item-object", + "type": "object", + "properties": { + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/server" + } + }, + "parameters": { + "$ref": "#/$defs/parameters" + }, + "additionalOperations": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/operation" + }, + "propertyNames": { + "$comment": "RFC9110 restricts methods to \"1*tchar\" in ABNF", + "pattern": "^[a-zA-Z0-9!#$%&'*+.^_`|~-]+$", + "not": { + "enum": [ + "GET", + "PUT", + "POST", + "DELETE", + "OPTIONS", + "HEAD", + "PATCH", + "TRACE", + "QUERY" + ] + } + } + }, + "get": { + "$ref": "#/$defs/operation" + }, + "put": { + "$ref": "#/$defs/operation" + }, + "post": { + "$ref": "#/$defs/operation" + }, + "delete": { + "$ref": "#/$defs/operation" + }, + "options": { + "$ref": "#/$defs/operation" + }, + "head": { + "$ref": "#/$defs/operation" + }, + "patch": { + "$ref": "#/$defs/operation" + }, + "trace": { + "$ref": "#/$defs/operation" + }, + "query": { + "$ref": "#/$defs/operation" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "operation": { + "$comment": "https://spec.openapis.org/oas/v3.2#operation-object", + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/$defs/external-documentation" + }, + "operationId": { + "type": "string" + }, + "parameters": { + "$ref": "#/$defs/parameters" + }, + "requestBody": { + "$ref": "#/$defs/request-body-or-reference" + }, + "responses": { + "$ref": "#/$defs/responses" + }, + "callbacks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/callbacks-or-reference" + } + }, + "deprecated": { + "default": false, + "type": "boolean" + }, + "security": { + "type": "array", + "items": { + "$ref": "#/$defs/security-requirement" + } + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/server" + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "external-documentation": { + "$comment": "https://spec.openapis.org/oas/v3.2#external-documentation-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + }, + "required": [ + "url" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/$defs/parameter-or-reference" + }, + "not": { + "allOf": [ + { + "contains": { + "type": "object", + "properties": { + "in": { + "const": "query" + } + }, + "required": [ + "in" + ] + } + }, + { + "contains": { + "type": "object", + "properties": { + "in": { + "const": "querystring" + } + }, + "required": [ + "in" + ] + } + } + ] + }, + "contains": { + "type": "object", + "properties": { + "in": { + "const": "querystring" + } + }, + "required": [ + "in" + ] + }, + "minContains": 0, + "maxContains": 1 + }, + "parameter": { + "$comment": "https://spec.openapis.org/oas/v3.2#parameter-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "in": { + "enum": [ + "query", + "querystring", + "header", + "path", + "cookie" + ] + }, + "description": { + "type": "string" + }, + "required": { + "default": false, + "type": "boolean" + }, + "deprecated": { + "default": false, + "type": "boolean" + }, + "schema": { + "$dynamicRef": "#meta" + }, + "content": { + "$ref": "#/$defs/content", + "minProperties": 1, + "maxProperties": 1 + } + }, + "required": [ + "name", + "in" + ], + "oneOf": [ + { + "required": [ + "schema" + ] + }, + { + "required": [ + "content" + ] + } + ], + "allOf": [ + { + "$ref": "#/$defs/examples" + }, + { + "$ref": "#/$defs/specification-extensions" + }, + { + "if": { + "properties": { + "in": { + "const": "query" + } + } + }, + "then": { + "properties": { + "allowEmptyValue": { + "default": false, + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "in": { + "const": "querystring" + } + } + }, + "then": { + "required": [ + "content" + ] + } + } + ], + "dependentSchemas": { + "schema": { + "properties": { + "style": { + "type": "string" + }, + "explode": { + "type": "boolean" + } + }, + "allOf": [ + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-path" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-header" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-query" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-cookie" + } + ], + "$defs": { + "styles-for-path": { + "if": { + "properties": { + "in": { + "const": "path" + } + } + }, + "then": { + "properties": { + "name": { + "pattern": "^[^{}]+$" + }, + "style": { + "default": "simple", + "enum": [ + "matrix", + "label", + "simple" + ] + }, + "required": { + "const": true + }, + "explode": { + "default": false + }, + "allowReserved": { + "type": "boolean", + "default": false + } + }, + "required": [ + "required" + ] + } + }, + "styles-for-header": { + "if": { + "properties": { + "in": { + "const": "header" + } + } + }, + "then": { + "properties": { + "style": { + "default": "simple", + "const": "simple" + }, + "explode": { + "default": false + } + } + } + }, + "styles-for-query": { + "if": { + "properties": { + "in": { + "const": "query" + } + } + }, + "then": { + "properties": { + "style": { + "default": "form", + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + }, + "allowReserved": { + "type": "boolean", + "default": false + } + }, + "$ref": "#/$defs/explode-for-form" + } + }, + "styles-for-cookie": { + "if": { + "properties": { + "in": { + "const": "cookie" + } + } + }, + "then": { + "properties": { + "style": { + "default": "form", + "enum": [ + "form", + "cookie" + ] + }, + "explode": { + "default": true + } + }, + "if": { + "properties": { + "style": { + "const": "form" + } + } + }, + "then": { + "properties": { + "allowReserved": { + "type": "boolean", + "default": false + } + } + } + } + } + } + } + }, + "unevaluatedProperties": false + }, + "parameter-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/parameter" + } + }, + "request-body": { + "$comment": "https://spec.openapis.org/oas/v3.2#request-body-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "content": { + "$ref": "#/$defs/content" + }, + "required": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "content" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "request-body-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/request-body" + } + }, + "content": { + "$comment": "https://spec.openapis.org/oas/v3.2#fixed-fields-10", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/media-type-or-reference" + }, + "propertyNames": { + "format": "media-range" + } + }, + "media-type": { + "$comment": "https://spec.openapis.org/oas/v3.2#media-type-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "schema": { + "$dynamicRef": "#meta" + }, + "itemSchema": { + "$dynamicRef": "#meta" + }, + "encoding": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/encoding" + } + }, + "prefixEncoding": { + "type": "array", + "items": { + "$ref": "#/$defs/encoding" + } + }, + "itemEncoding": { + "$ref": "#/$defs/encoding" + } + }, + "dependentSchemas": { + "encoding": { + "properties": { + "prefixEncoding": false, + "itemEncoding": false + } + } + }, + "allOf": [ + { + "$ref": "#/$defs/examples" + }, + { + "$ref": "#/$defs/specification-extensions" + } + ], + "unevaluatedProperties": false + }, + "media-type-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/media-type" + } + }, + "encoding": { + "$comment": "https://spec.openapis.org/oas/v3.2#encoding-object", + "type": "object", + "properties": { + "contentType": { + "type": "string", + "format": "media-range" + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/header-or-reference" + } + }, + "style": { + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean" + }, + "encoding": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/encoding" + } + }, + "prefixEncoding": { + "type": "array", + "items": { + "$ref": "#/$defs/encoding" + } + }, + "itemEncoding": { + "$ref": "#/$defs/encoding" + } + }, + "dependentSchemas": { + "encoding": { + "properties": { + "prefixEncoding": false, + "itemEncoding": false + } + }, + "style": { + "properties": { + "allowReserved": { + "default": false + } + }, + "$ref": "#/$defs/explode-for-form" + }, + "explode": { + "properties": { + "style": { + "default": "form" + }, + "allowReserved": { + "default": false + } + } + }, + "allowReserved": { + "properties": { + "style": { + "default": "form" + } + }, + "$ref": "#/$defs/explode-for-form" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "responses": { + "$comment": "https://spec.openapis.org/oas/v3.2#responses-object", + "type": "object", + "properties": { + "default": { + "$ref": "#/$defs/response-or-reference" + } + }, + "patternProperties": { + "^[1-5](?:[0-9]{2}|XX)$": { + "$ref": "#/$defs/response-or-reference" + } + }, + "minProperties": 1, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false, + "if": { + "$comment": "either default, or at least one response code property must exist", + "patternProperties": { + "^[1-5](?:[0-9]{2}|XX)$": false + } + }, + "then": { + "required": [ + "default" + ] + } + }, + "response": { + "$comment": "https://spec.openapis.org/oas/v3.2#response-object", + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/header-or-reference" + } + }, + "content": { + "$ref": "#/$defs/content" + }, + "links": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/link-or-reference" + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "response-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/response" + } + }, + "callbacks": { + "$comment": "https://spec.openapis.org/oas/v3.2#callback-object", + "type": "object", + "$ref": "#/$defs/specification-extensions", + "additionalProperties": { + "$ref": "#/$defs/path-item" + } + }, + "callbacks-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/callbacks" + } + }, + "example": { + "$comment": "https://spec.openapis.org/oas/v3.2#example-object", + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dataValue": true, + "serializedValue": { + "type": "string" + }, + "value": true, + "externalValue": { + "type": "string", + "format": "uri-reference" + } + }, + "allOf": [ + { + "not": { + "required": [ + "value", + "externalValue" + ] + } + }, + { + "not": { + "required": [ + "value", + "dataValue" + ] + } + }, + { + "not": { + "required": [ + "value", + "serializedValue" + ] + } + }, + { + "not": { + "required": [ + "serializedValue", + "externalValue" + ] + } + } + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "example-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/example" + } + }, + "link": { + "$comment": "https://spec.openapis.org/oas/v3.2#link-object", + "type": "object", + "properties": { + "operationRef": { + "type": "string", + "format": "uri-reference" + }, + "operationId": { + "type": "string" + }, + "parameters": { + "$ref": "#/$defs/map-of-strings" + }, + "requestBody": true, + "description": { + "type": "string" + }, + "server": { + "$ref": "#/$defs/server" + } + }, + "oneOf": [ + { + "required": [ + "operationRef" + ] + }, + { + "required": [ + "operationId" + ] + } + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "link-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/link" + } + }, + "header": { + "$comment": "https://spec.openapis.org/oas/v3.2#header-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "required": { + "default": false, + "type": "boolean" + }, + "deprecated": { + "default": false, + "type": "boolean" + }, + "schema": { + "$dynamicRef": "#meta" + }, + "content": { + "$ref": "#/$defs/content", + "minProperties": 1, + "maxProperties": 1 + } + }, + "oneOf": [ + { + "required": [ + "schema" + ] + }, + { + "required": [ + "content" + ] + } + ], + "dependentSchemas": { + "schema": { + "properties": { + "style": { + "default": "simple", + "const": "simple" + }, + "explode": { + "default": false, + "type": "boolean" + } + } + } + }, + "allOf": [ + { + "$ref": "#/$defs/examples" + }, + { + "$ref": "#/$defs/specification-extensions" + } + ], + "unevaluatedProperties": false + }, + "header-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/header" + } + }, + "tag": { + "$comment": "https://spec.openapis.org/oas/v3.2#tag-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/$defs/external-documentation" + }, + "parent": { + "type": "string" + }, + "kind": { + "type": "string" + } + }, + "required": [ + "name" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "reference": { + "$comment": "https://spec.openapis.org/oas/v3.2#reference-object", + "type": "object", + "properties": { + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "schema": { + "$comment": "https://spec.openapis.org/oas/v3.2#schema-object", + "$dynamicAnchor": "meta", + "type": [ + "object", + "boolean" + ] + }, + "security-scheme": { + "$comment": "https://spec.openapis.org/oas/v3.2#security-scheme-object", + "type": "object", + "properties": { + "type": { + "enum": [ + "apiKey", + "http", + "mutualTLS", + "oauth2", + "openIdConnect" + ] + }, + "description": { + "type": "string" + }, + "deprecated": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "type" + ], + "allOf": [ + { + "$ref": "#/$defs/specification-extensions" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-apikey" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-http" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-http-bearer" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-oauth2" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-oidc" + } + ], + "unevaluatedProperties": false, + "$defs": { + "type-apikey": { + "if": { + "properties": { + "type": { + "const": "apiKey" + } + } + }, + "then": { + "properties": { + "name": { + "type": "string" + }, + "in": { + "enum": [ + "query", + "header", + "cookie" + ] + } + }, + "required": [ + "name", + "in" + ] + } + }, + "type-http": { + "if": { + "properties": { + "type": { + "const": "http" + } + } + }, + "then": { + "properties": { + "scheme": { + "type": "string" + } + }, + "required": [ + "scheme" + ] + } + }, + "type-http-bearer": { + "if": { + "properties": { + "type": { + "const": "http" + }, + "scheme": { + "type": "string", + "pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$" + } + }, + "required": [ + "type", + "scheme" + ] + }, + "then": { + "properties": { + "bearerFormat": { + "type": "string" + } + } + } + }, + "type-oauth2": { + "if": { + "properties": { + "type": { + "const": "oauth2" + } + } + }, + "then": { + "properties": { + "flows": { + "$ref": "#/$defs/oauth-flows" + }, + "oauth2MetadataUrl": { + "type": "string", + "format": "uri-reference" + } + }, + "required": [ + "flows" + ] + } + }, + "type-oidc": { + "if": { + "properties": { + "type": { + "const": "openIdConnect" + } + } + }, + "then": { + "properties": { + "openIdConnectUrl": { + "type": "string", + "format": "uri-reference" + } + }, + "required": [ + "openIdConnectUrl" + ] + } + } + } + }, + "security-scheme-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/security-scheme" + } + }, + "oauth-flows": { + "type": "object", + "properties": { + "implicit": { + "$ref": "#/$defs/oauth-flows/$defs/implicit" + }, + "password": { + "$ref": "#/$defs/oauth-flows/$defs/password" + }, + "clientCredentials": { + "$ref": "#/$defs/oauth-flows/$defs/client-credentials" + }, + "authorizationCode": { + "$ref": "#/$defs/oauth-flows/$defs/authorization-code" + }, + "deviceAuthorization": { + "$ref": "#/$defs/oauth-flows/$defs/device-authorization" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false, + "$defs": { + "implicit": { + "type": "object", + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "authorizationUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "password": { + "type": "object", + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "client-credentials": { + "type": "object", + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "authorization-code": { + "type": "object", + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "authorizationUrl", + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "device-authorization": { + "type": "object", + "properties": { + "deviceAuthorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "deviceAuthorizationUrl", + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + } + } + }, + "security-requirement": { + "$comment": "https://spec.openapis.org/oas/v3.2#security-requirement-object", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "specification-extensions": { + "$comment": "https://spec.openapis.org/oas/v3.2#specification-extensions", + "patternProperties": { + "^x-": true + } + }, + "examples": { + "properties": { + "example": true, + "examples": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/example-or-reference" + } + } + }, + "not": { + "required": [ + "example", + "examples" + ] + } + }, + "map-of-strings": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "explode-for-form": { + "$comment": "for encoding objects, and query and cookie parameters, style=form is the default", + "if": { + "properties": { + "style": { + "const": "form" + } + } + }, + "then": { + "properties": { + "explode": { + "default": true + } + } + }, + "else": { + "properties": { + "explode": { + "default": false + } + } + } + } + } +} diff --git a/src/OpenAPI.jl b/src/OpenAPI.jl index 9366a77..4cb17eb 100644 --- a/src/OpenAPI.jl +++ b/src/OpenAPI.jl @@ -1,27 +1,56 @@ -module OpenAPI +""" +OpenAPI.jl: build OpenAPI documents from declared endpoints, and generate Julia +clients from OpenAPI documents. + +Two pieces: -using HTTP, JSON, URIs, Dates, TimeZones, Base64 -using Downloads -using p7zip_jll +1. **Document generation** — describe endpoints as [`OpenAPI.Operation`](@ref)s + and get a valid OpenAPI 3.2.0 document. Framework packages can add router + adapters through the `operations` and `register!` extension seams. +2. **Client generation** — [`OpenAPI.client`](@ref) turns an OpenAPI 3.0, 3.1, + or 3.2 document (built in-process, read from JSON or YAML, or fetched from a + running app) into a deterministic single-file Julia client. Generated + modules use HTTP.jl for transport and JSON.jl plus OpenAPI's provisional + schema engine for typed, validated request and response handling. +""" +module OpenAPI -import Base: getindex, keys, length, iterate, hasproperty -import JSON: lower +using Dates, JSON, SHA +import YAML +const OPENAPI_VERSION = "3.2.0" -const _JSON_PARSE_ISROOT_SUPPORTED = try; JSON.parse("1 "; isroot=false); true; catch; false; end +include("schema_engine/SchemaEngine.jl") -if _JSON_PARSE_ISROOT_SUPPORTED - _json_parse(io_or_str) = JSON.parse(io_or_str; isroot=false) -else - _json_parse(io_or_str) = JSON.parse(io_or_str) -end +public SchemaEngine -include("commontypes.jl") -include("datetime.jl") -include("val.jl") -include("json.jl") +include("schemas.jl") +include("document.jl") +include("diagnostics.jl") +include("source_locations.jl") +include("loading.jl") +include("references.jl") +include("normalize.jl") +include("planning.jl") +include("read.jl") +include("runtime.jl") include("client.jl") -include("server.jl") -include("tools.jl") + +# ── extension seams ───────────────────────────────────────────────────────── + +""" + OpenAPI.register!(integration; kwargs...) + +Extension seam for downstream server frameworks that expose a generated OpenAPI +document. OpenAPI.jl itself does not depend on a server framework. +""" +function register! end + +"""Extension seam for converting framework routes to `Vector{Operation}`.""" +function operations end + +# implemented by OpenAPIHTTPExt (loaded with the HTTP package) +function fetchurl end +function fetchresource end end # module diff --git a/src/client.jl b/src/client.jl index 906192b..b8d27c7 100644 --- a/src/client.jl +++ b/src/client.jl @@ -1,445 +1,3162 @@ -module Clients - -using Downloads -using URIs -using JSON -using MbedTLS -using Dates -using TimeZones -using LibCURL -using HTTP -using MIMEs - -import Base: convert, show, summary, getproperty, setproperty!, iterate -import ..OpenAPI: APIModel, UnionAPIModel, OneOfAPIModel, AnyOfAPIModel, APIClientImpl, OpenAPIException, InvocationException, to_json, from_json, validate_property, property_type -import ..OpenAPI: str2zoneddatetime, str2datetime, str2date, _json_parse - -include("client/clienttypes.jl") -include("client/chunk_readers.jl") -include("client/httplibs/httplibs.jl") +const GENERATED_RUNTIME = raw""" +struct Absent end +const ABSENT = Absent() +Base.show(io::IO, ::Absent) = print(io, "ABSENT") -""" - set_user_agent(client::Client, ua::String) +struct DecodeError <: Exception + message::String +end +Base.showerror(io::IO, error::DecodeError) = print(io, error.message) + +struct ApiError <: Exception + operation_id::String + status::Int + headers::Vector{Pair{String,String}} + decoded_headers::Dict{String,Any} + body::Vector{UInt8} + decoded::Any + decode_error::Any +end +function Base.showerror(io::IO, error::ApiError) + print(io, "ApiError(", error.status, ") in ", error.operation_id) + if error.decode_error !== nothing + print(io, " (response decoding failed: ") + showerror(io, error.decode_error) + print(io, ')') + end + isempty(error.body) && return + text = isvalid(String, error.body) ? String(copy(error.body)) : nothing + if text === nothing + preview = bytes2hex(error.body[1:min(length(error.body), 32)]) + print(io, ": ", length(error.body), " binary bytes (", preview) + length(error.body) > 32 && print(io, "…") + print(io, ')') + else + preview = first(text, min(length(text), 512)) + print(io, ": ", preview) + length(text) > 512 && print(io, "…") + end +end -Set the User-Agent header to be sent with all API calls. -""" -set_user_agent(client::Client, ua::String) = set_header(client, "User-Agent", ua) +struct ApiResponse{T} + status::Int + headers::Vector{Pair{String,String}} + decoded_headers::Dict{String,Any} + body::T +end -""" - set_cookie(client::Client, ck::String) +struct UnexpectedBody <: Exception + operation_id::String + status::Int + bytes::Int +end +Base.showerror(io::IO, error::UnexpectedBody) = print( + io, + "operation ", + error.operation_id, + " received an undocumented ", + error.bytes, + "-byte response body for status ", + error.status, +) + +struct UnsupportedMediaType <: Exception + media_type::String + direction::Symbol +end +Base.showerror(io::IO, error::UnsupportedMediaType) = print( + io, + "no ", + error.direction, + " codec is configured for media type ", + repr(error.media_type), +) + +struct UnexpectedContentType <: Exception + operation_id::String + status::Int + received::String + expected::Tuple +end +function Base.showerror(io::IO, error::UnexpectedContentType) + print( + io, + "unexpected Content-Type ", + repr(error.received), + " for ", + error.operation_id, + " response ", + error.status, + "; expected ", + join(error.expected, ", "), + ) +end -Set the Cookie header to be sent with all API calls. -""" -set_cookie(client::Client, ck::String) = set_header(client, "Cookie", ck) +struct SchemaValidationError <: Exception + context::String + issues::Vector{Any} +end +function Base.showerror(io::IO, error::SchemaValidationError) + print(io, "schema validation failed while ", error.context) + for issue in error.issues + print(io, "\n ", issue.path, ": ", issue.reason) + end +end -""" - set_header(client::Client, name::String, value::String) +const _SCHEMA_GRAPHS = Dict{Symbol,Any}() +const _SCHEMA_GRAPH_LOCK = ReentrantLock() + +function _schema_graph(direction::Symbol = :neutral) + direction in (:neutral, :input, :output) || + throw(ArgumentError("schema direction must be :neutral, :input, or :output")) + isempty(_SCHEMA_ROOT_DATA) && return nothing + haskey(_SCHEMA_GRAPHS, direction) && return _SCHEMA_GRAPHS[direction] + return lock(_SCHEMA_GRAPH_LOCK) do + haskey(_SCHEMA_GRAPHS, direction) && return _SCHEMA_GRAPHS[direction] + documents = Dict{String,Any}( + entry.id => JSON.parse(entry.json; duplicate_keys = :error) for + entry in _SCHEMA_RESOURCE_DATA + ) + if direction !== :neutral + for rule in _SCHEMA_DIRECTIONAL_REQUIRED + removed = direction === :input ? rule.input : rule.output + isempty(removed) && continue + document = get(documents, rule.resource, nothing) + document === nothing && continue + schema = SchemaEngine.Resources.resolve( + document, + SchemaEngine.Resources.JSONPointer(rule.pointer), + ) + schema isa AbstractDict || continue + required = get(schema, "required", nothing) + required isa AbstractVector || continue + retained = Any[ + name for name in required if String(name) ∉ removed + ] + isempty(retained) ? delete!(schema, "required") : + (schema["required"] = retained) + end + end + resources = SchemaEngine.Resources.Resource[] + for entry in _SCHEMA_RESOURCE_DATA + id = SchemaEngine.Resources.ResourceId(entry.id) + retrieval = SchemaEngine.Resources.ResourceId(entry.retrieval) + push!( + resources, + SchemaEngine.Resources.Resource( + id, + documents[entry.id]; + retrieval, + media_type = entry.media_type, + ), + ) + end + roots = SchemaEngine.Resources.NodeId[ + SchemaEngine.Resources.NodeId( + SchemaEngine.Resources.ResourceId(entry.resource), + SchemaEngine.Resources.JSONPointer(entry.pointer), + ) for entry in _SCHEMA_ROOT_DATA + ] + root_dialects = Dict( + root => entry.dialect for (root, entry) in zip(roots, _SCHEMA_ROOT_DATA) + ) + dialect_aliases = Dict( + entry.uri => SchemaEngine.Dialect( + entry.name, + entry.uri, + entry.id_keyword, + entry.ref_siblings, + entry.modern_items, + entry.unevaluated, + entry.dynamic_refs, + entry.recursive_refs, + entry.applicator, + entry.validation, + ) for entry in _SCHEMA_DIALECT_DATA + ) + graph = SchemaEngine.CompiledSchemas( + resources, + roots; + dialect = SchemaEngine.DRAFT202012, + root_dialects, + dialect_aliases, + ) + _SCHEMA_GRAPHS[direction] = graph + return graph + end +end -Set the specified header to be sent with all API calls. -""" -set_header(client::Client, name::String, value::String) = (client.headers[name] = value) +function _schema_at(descriptor, direction::Symbol = :neutral) + descriptor === nothing && return nothing + graph = _schema_graph(direction) + graph === nothing && return nothing + node = SchemaEngine.Resources.NodeId( + SchemaEngine.Resources.ResourceId(descriptor.resource), + SchemaEngine.Resources.JSONPointer(descriptor.pointer), + ) + return SchemaEngine.subschema(graph, node) +end -""" - set_timeout(client::Client, timeout::Int) +function _schema_issues(descriptor, value; direction::Symbol = :neutral) + schema = _schema_at(descriptor, direction) + schema === nothing && return Any[] + return Any[SchemaEngine.validate(schema, value; fail_fast = false)...] +end -Set the timeout in seconds for all API calls. -""" -set_timeout(client::Client, timeout::Int) = (client.timeout[] = timeout) +function _validate_schema( + descriptor, + value, + context; + direction::Symbol = :neutral, +) + issues = _schema_issues(descriptor, value; direction) + isempty(issues) || throw(SchemaValidationError(String(context), issues)) + return value +end + +_schema_valid(descriptor, value; direction::Symbol = :neutral) = + isempty(_schema_issues(descriptor, value; direction)) + +abstract type AbstractCredential end +struct ApiKeyCredential <: AbstractCredential + value::String + authorizations::Set{String} +end +ApiKeyCredential(value::AbstractString; roles = String[]) = + ApiKeyCredential(String(value), Set(String.(roles))) +struct BasicCredential <: AbstractCredential + username::String + password::String + authorizations::Set{String} +end +BasicCredential(username::AbstractString, password::AbstractString; roles = String[]) = + BasicCredential(String(username), String(password), Set(String.(roles))) +struct BearerCredential <: AbstractCredential + token::String + authorizations::Set{String} +end +function BearerCredential(token::AbstractString; scopes = String[], roles = String[]) + return BearerCredential( + String(token), + union(Set(String.(scopes)), Set(String.(roles))), + ) +end +struct HttpCredential <: AbstractCredential + value::String + authorizations::Set{String} +end +HttpCredential(value::AbstractString; roles = String[]) = + HttpCredential(String(value), Set(String.(roles))) +struct MutualTLSCredential <: AbstractCredential + request_options::NamedTuple + authorizations::Set{String} +end +MutualTLSCredential(request_options::NamedTuple; roles = String[]) = + MutualTLSCredential(request_options, Set(String.(roles))) + +struct Upload + data::Vector{UInt8} + filename::Union{Nothing,String} + content_type::Union{Nothing,String} + headers::Vector{Pair{String,String}} +end + +# Header values for one multipart part and, when needed, its nested parts. +struct MultipartPartHeaders + values::Any + parts::Any +end +MultipartPartHeaders(values = NamedTuple(); parts = NamedTuple()) = + MultipartPartHeaders(values, parts) +function Upload( + data::AbstractVector{UInt8}; + filename::Union{Nothing,AbstractString} = nothing, + content_type::Union{Nothing,AbstractString} = nothing, + headers = Pair{String,String}[], +) + return Upload( + Vector{UInt8}(data), + filename === nothing ? nothing : String(filename), + content_type === nothing ? nothing : String(content_type), + Pair{String,String}[String(key) => String(value) for (key, value) in headers], + ) +end + +mutable struct Client + server::Union{Nothing,String} + server_index::Int + server_name::Union{Nothing,String} + server_variables::Dict{String,String} + credentials::Dict{String,AbstractCredential} + headers::Vector{Pair{String,String}} + request_options::NamedTuple + require_credentials::Bool + media_encoders::Dict{String,Function} + media_decoders::Dict{String,Function} + validate_requests::Bool + validate_responses::Bool +end + +function _normalize_media_codecs(codecs::Dict{String,Function}) + normalized = Dict{String,Function}() + for name in keys(codecs) + normalized[_base_media_type(name)] = codecs[name] + end + return normalized +end + +function _normalize_media_codecs(codecs::AbstractDict) + return Dict{String,Function}( + _base_media_type(name) => codec for (name, codec) in codecs + ) +end + +function Client( + server::Union{Nothing,AbstractString} = nothing; + server_index::Integer = 1, + server_name::Union{Nothing,AbstractString} = nothing, + server_variables::AbstractDict = Dict{String,String}(), + credentials::AbstractDict = Dict{String,AbstractCredential}(), + headers = Pair{String,String}[], + request_options::NamedTuple = NamedTuple(), + require_credentials::Bool = true, + media_encoders::AbstractDict = Dict{String,Function}(), + media_decoders::AbstractDict = Dict{String,Function}(), + validate_requests::Bool = true, + validate_responses::Bool = true, +) + server_index > 0 || throw(ArgumentError("server_index must be positive")) + normalized_credentials = credentials isa Dict{String,AbstractCredential} ? + copy(credentials) : + Dict{String,AbstractCredential}( + String(name) => credential for (name, credential) in credentials + ) + normalized_headers = headers isa Vector{Pair{String,String}} ? copy(headers) : + Pair{String,String}[ + String(name) => String(value) for (name, value) in headers + ] + return Client( + server === nothing ? nothing : String(rstrip(server, '/')), + Int(server_index), + server_name === nothing ? nothing : String(server_name), + server_variables isa Dict{String,String} ? copy(server_variables) : + Dict{String,String}( + String(name) => String(value) for (name, value) in server_variables + ), + normalized_credentials, + normalized_headers, + request_options, + require_credentials, + _normalize_media_codecs(media_encoders), + _normalize_media_codecs(media_decoders), + validate_requests, + validate_responses, + ) +end + +function credential!(client::Client, name::AbstractString, credential::AbstractCredential) + client.credentials[String(name)] = credential + return client +end +credential!(name::AbstractString, credential::AbstractCredential) = + credential!(DEFAULT_CLIENT, name, credential) + +function clearcredential!(client::Client, name::AbstractString) + delete!(client.credentials, String(name)) + return client +end +clearcredential!(name::AbstractString) = clearcredential!(DEFAULT_CLIENT, name) + +function server!(client::Client, server::Union{Nothing,AbstractString}) + client.server = server === nothing ? nothing : String(rstrip(server, '/')) + isdefined(@__MODULE__, :DEFAULT_CLIENT) && client === DEFAULT_CLIENT && + (SERVER[] = something(client.server, _DEFAULT_SERVER)) + return client +end +server!(server::Union{Nothing,AbstractString}) = server!(DEFAULT_CLIENT, server) + +function server_index!(client::Client, index::Integer) + index > 0 || throw(ArgumentError("server index must be positive")) + client.server_index = Int(index) + client.server_name = nothing + return client +end +server_index!(index::Integer) = server_index!(DEFAULT_CLIENT, index) + +function server_name!(client::Client, name::Union{Nothing,AbstractString}) + client.server_name = name === nothing ? nothing : String(name) + return client +end +server_name!(name::Union{Nothing,AbstractString}) = server_name!(DEFAULT_CLIENT, name) -function with_timeout(fn, client::Client, timeout::Integer) - oldtimeout = client.timeout[] - client.timeout[] = timeout +function server_variable!(client::Client, name::AbstractString, value::AbstractString) + client.server_variables[String(name)] = String(value) + return client +end +server_variable!(name::AbstractString, value::AbstractString) = + server_variable!(DEFAULT_CLIENT, name, value) + +function codec!( + client::Client, + media_type::AbstractString; + encode::Union{Nothing,Function} = nothing, + decode::Union{Nothing,Function} = nothing, +) + key = _base_media_type(media_type) + encode === nothing || (client.media_encoders[key] = encode) + decode === nothing || (client.media_decoders[key] = decode) + return client +end +codec!(media_type::AbstractString; kwargs...) = + codec!(DEFAULT_CLIENT, media_type; kwargs...) + +function authorization!(client::Client, token::Union{Nothing,AbstractString}) + names = String[ + name for (name, scheme) in _SECURITY_SCHEMES + if scheme.type in (:http_bearer, :oauth2, :openidconnect) + ] + isempty(names) && throw(ArgumentError("the OpenAPI document has no bearer-compatible security scheme")) + for name in names + if token === nothing + delete!(client.credentials, name) + else + client.credentials[name] = BearerCredential(String(token)) + end + end + return client +end +authorization!(token::Union{Nothing,AbstractString}) = authorization!(DEFAULT_CLIENT, token) + +_typename(::Type{T}) where {T} = string(T) +_required(value, key, model) = haskey(value, key) ? value[key] : + throw(DecodeError("required field $(repr(key)) is missing while decoding $model")) +_object(value, model) = value isa AbstractDict ? value : + throw(DecodeError("expected an object while decoding $model, got $(typeof(value))")) + +_decode(::Type{Any}, value) = value +_decode(::Type{Nothing}, ::Nothing) = nothing +_decode(::Type{Nothing}, value) = throw(DecodeError("expected null, got $(typeof(value))")) +_decode(::Type{String}, value::AbstractString) = String(value) +_decode(::Type{Bool}, value::Bool) = value +_decode(::Type{T}, value::Integer) where {T<:Integer} = try + value isa Bool && throw(DecodeError("expected an integer, got Bool")) + convert(T, value) +catch + throw(DecodeError("integer $value does not fit $T")) +end +function _decode(::Type{T}, value::Real) where {T<:AbstractFloat} + value isa Bool && throw(DecodeError("expected a number, got Bool")) + return convert(T, value) +end +function _decode(::Type{Dates.Date}, value::AbstractString) + try + return Dates.Date(value) + catch error + throw(DecodeError("invalid RFC 3339 full-date: $(sprint(showerror, error))")) + end +end +function _decode(::Type{Dates.Time}, value::AbstractString) try - fn(client) - finally - client.timeout[] = oldtimeout + return Dates.Time(value) + catch error + throw(DecodeError("invalid time: $(sprint(showerror, error))")) + end +end +function _decode(::Type{Dates.DateTime}, value::AbstractString) + matched = match( + r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})$", + value, + ) + matched === nothing && throw(DecodeError("invalid RFC 3339 date-time $(repr(value))")) + try + output = Dates.DateTime(matched.captures[1]) + fraction = matched.captures[2] + if fraction !== nothing + milliseconds = parse(Int, rpad(first(fraction, min(3, length(fraction))), 3, '0')) + output += Dates.Millisecond(milliseconds) + end + zone = matched.captures[3] + if zone != "Z" + sign = startswith(zone, '+') ? 1 : -1 + hours = parse(Int, zone[2:3]) + minutes = parse(Int, zone[5:6]) + output -= Dates.Minute(sign * (60 * hours + minutes)) + end + return output + catch error + error isa DecodeError && rethrow() + throw(DecodeError("invalid RFC 3339 date-time: $(sprint(showerror, error))")) + end +end +function _decode(::Type{UUIDs.UUID}, value::AbstractString) + try + return UUIDs.UUID(value) + catch error + throw(DecodeError("invalid UUID: $(sprint(showerror, error))")) + end +end +function _decode(::Type{Vector{UInt8}}, value::AbstractString) + try + return Base64.base64decode(value) + catch error + throw(DecodeError("invalid base64 data: $(sprint(showerror, error))")) + end +end +_decode(::Type{Vector{UInt8}}, value::AbstractVector{UInt8}) = copy(value) + +function _decode(::Type{T}, value::AbstractVector) where {T<:AbstractVector} + element = eltype(T) + return T([_decode(element, item) for item in value]) +end +function _decode(::Type{T}, value::AbstractDict) where {T<:AbstractDict} + keytype(T) <: AbstractString || + throw(DecodeError("only string-key dictionaries are supported, got $T")) + output = T() + for (key, item) in value + output[convert(keytype(T), key)] = _decode(valtype(T), item) + end + return output +end +function _decode(::Type{T}, value::AbstractVector) where {T<:Tuple} + length(value) == fieldcount(T) || + throw(DecodeError("expected $(fieldcount(T)) tuple items, got $(length(value))")) + return T((_decode(fieldtype(T, index), value[index]) for index in 1:fieldcount(T))...) +end + +function _direct_union_match(::Type{T}, value) where {T} + T === Nothing && return value === nothing + T === Bool && return value isa Bool + T <: Integer && return value isa Integer && !(value isa Bool) + T <: AbstractFloat && return value isa AbstractFloat + T <: AbstractString && return value isa AbstractString + T <: AbstractVector && return value isa AbstractVector + T <: AbstractDict && return value isa AbstractDict + return value isa T +end + +function _decode_union(::Type{T}, value; oneof::Bool = false) where {T} + variants = Base.uniontypes(T) + if value === nothing && Nothing in variants + return nothing + end + if !oneof + preferred = Any[ + variant for variant in variants + if variant ∉ (Absent, Nothing) && _direct_union_match(variant, value) + ] + append!(preferred, Any[variant for variant in variants if variant ∉ preferred]) + variants = preferred + end + successes = Any[] + failures = String[] + for variant in variants + variant in (Absent, Nothing) && continue + try + decoded = _decode(variant, value) + oneof || return decoded + push!(successes, decoded) + catch error + error isa DecodeError || rethrow() + push!(failures, error.message) + end end + length(successes) == 1 && return only(successes) + isempty(successes) && throw( + DecodeError("value does not match any variant of $T: " * join(failures, "; ")), + ) + throw(DecodeError("value matches more than one variant of $T")) end -function with_timeout(fn, api::APIClientImpl, timeout::Integer) - client = api.client - oldtimeout = client.timeout[] - client.timeout[] = timeout +function _decode(::Type{T}, value) where {T} + T isa Union && return _decode_union(T, value) + value isa T && return value try - fn(api) - finally - client.timeout[] = oldtimeout + return convert(T, value) + catch + throw(DecodeError("cannot decode $(typeof(value)) as $T")) + end +end + +_encode(::Absent) = throw(ArgumentError("ABSENT is only valid as an object field")) +_encode(::Nothing) = nothing +_encode(value::Union{Bool,Number,AbstractString}) = value +_encode(value::Union{Dates.Date,Dates.Time,UUIDs.UUID}) = string(value) +_encode(value::Dates.DateTime) = Dates.format(value, dateformat"yyyy-mm-ddTHH:MM:SS.sss") * "Z" +_encode(value::AbstractVector{UInt8}) = Base64.base64encode(value) +_encode(value::Upload) = Base64.base64encode(value.data) +_encode(value::AbstractVector) = Any[_encode(item) for item in value] +_encode(value::Tuple) = Any[_encode(item) for item in value] +function _encode(value::AbstractDict) + output = JSON.Object{String,Any}() + for (key, item) in value + output[String(key)] = _encode(item) + end + return output +end +function _encode(value::NamedTuple) + output = JSON.Object{String,Any}() + for (key, item) in pairs(value) + output[String(key)] = _encode(item) end + return output end +_encode(value) = value + +_scalar(::Nothing) = "" +_scalar(value::Bool) = value ? "true" : "false" +_scalar(value::Dates.DateTime) = _encode(value) +_scalar(value::Union{Dates.Date,Dates.Time,UUIDs.UUID}) = string(value) +_scalar(value::AbstractVector{UInt8}) = Base64.base64encode(value) +_scalar(value) = string(value) + +function _pairs(value) + lowered = _encode(value) + lowered isa AbstractDict || throw(ArgumentError("parameter style requires an object value")) + return Pair{String,Any}[String(key) => item for (key, item) in lowered] +end + +function _join_array(value, delimiter) + value isa AbstractVector || value isa Tuple || + throw(ArgumentError("parameter style requires an array value")) + return join((_scalar(item) for item in value), delimiter) +end + +function _join_object(value, pair_delimiter, key_delimiter) + return join( + (string(_scalar(key), key_delimiter, _scalar(item)) for (key, item) in _pairs(value)), + pair_delimiter, + ) +end + +_path_scalar(value) = _escape(_scalar(value)) +function _path_array(value, delimiter) + value isa AbstractVector || value isa Tuple || + throw(ArgumentError("parameter style requires an array value")) + return join((_path_scalar(item) for item in value), delimiter) +end -is_json_mime(mime::T) where {T <: AbstractString} = ("*/*" == mime) || occursin(r"(?i)application/json(;.*)?", mime) || occursin(r"(?i)application/(.*)\+json(;.*)?", mime) +function _path_object(value, pair_delimiter, key_delimiter) + return join( + ( + string( + _path_scalar(key), + key_delimiter, + _path_scalar(item), + ) for (key, item) in _pairs(value) + ), + pair_delimiter, + ) +end -function select_header_accept(accepts::Vector{String}) - isempty(accepts) && (return "application/json") - for accept in accepts - is_json_mime(accept) && (return accept) +function _path_parameter(name, value, style::Symbol, explode::Bool) + encoded = _encode(value) + if encoded === nothing + style === :matrix && return ";" * _path_scalar(name) + style === :label && return "." + style === :simple && return "" + end + if style === :simple + encoded isa AbstractDict && return explode ? + _path_object(encoded, ",", "=") : _path_object(encoded, ",", ",") + encoded isa AbstractVector && return _path_array(encoded, ",") + return _path_scalar(encoded) + elseif style === :label + encoded isa AbstractDict && return "." * (explode ? + _path_object(encoded, ".", "=") : _path_object(encoded, ",", ",")) + encoded isa AbstractVector && return "." * _path_array(encoded, explode ? "." : ",") + return "." * _path_scalar(encoded) + elseif style === :matrix + encoded_name = _path_scalar(name) + if encoded isa AbstractDict + return explode ? + join((";" * _path_scalar(key) * "=" * _path_scalar(item) for (key, item) in _pairs(encoded))) : + ";" * encoded_name * "=" * _path_object(encoded, ",", ",") + elseif encoded isa AbstractVector + return explode ? + join((";" * encoded_name * "=" * _path_scalar(item) for item in encoded)) : + ";" * encoded_name * "=" * _path_array(encoded, ",") + end + return ";" * encoded_name * "=" * _path_scalar(encoded) end - return join(accepts, ", ") + throw(ArgumentError("unsupported path parameter style $style")) +end + +function _form_component(value, allow_reserved) + encoded = _escape(_scalar(value); allow_reserved) + return replace(encoded, "," => "%2C") end -function select_header_content_type(ctypes::Vector{String}) - isempty(ctypes) && (return "application/json") - for ctype in ctypes - is_json_mime(ctype) && (return (("*/*" == ctype) ? "application/json" : ctype)) +function _query_parameter( + name, + value, + style::Symbol, + explode::Bool, + allow_reserved::Bool, +) + encoded = _encode(value) + if style === :deepObject + if encoded isa AbstractDict + any( + item -> item isa AbstractDict || item isa AbstractVector || item isa Tuple, + values(encoded), + ) && throw(ArgumentError("deepObject does not define nested object or array values")) + return Tuple{String,String,Bool}[ + (string(name, '[', key, ']'), _scalar(item), false) for + (key, item) in _pairs(encoded) + ] + elseif encoded isa AbstractVector || encoded isa Tuple + return Tuple{String,String,Bool}[ + (string(name, "[]"), _scalar(item), false) for item in encoded + ] + end + return [(name, _scalar(encoded), false)] + elseif style in (:spaceDelimited, :pipeDelimited) + explode && throw(ArgumentError("$style with explode=true is undefined")) + delimiter = style === :spaceDelimited ? " " : "|" + if encoded isa AbstractDict + return [(name, _join_object(encoded, delimiter, delimiter), false)] + end + return [(name, _join_array(encoded, delimiter), false)] + elseif style === :form + if encoded isa AbstractDict + return explode ? + Tuple{String,String,Bool}[ + (_scalar(key), _scalar(item), false) for + (key, item) in _pairs(encoded) + ] : + [ + ( + name, + join( + ( + _form_component(key, allow_reserved) * "," * + _form_component(item, allow_reserved) for + (key, item) in _pairs(encoded) + ), + ",", + ), + true, + ), + ] + elseif encoded isa AbstractVector + return explode ? + Tuple{String,String,Bool}[ + (name, _scalar(item), false) for item in encoded + ] : + [ + ( + name, + join( + (_form_component(item, allow_reserved) for item in encoded), + ",", + ), + true, + ), + ] + end + return [(name, _scalar(encoded), false)] end - return ctypes[1] + throw(ArgumentError("unsupported query parameter style $style")) end -set_header_accept(ctx::Ctx, accepts::Vector{T}) where {T} = set_header_accept(ctx, convert(Vector{String}, accepts)) -function set_header_accept(ctx::Ctx, accepts::Vector{String}) - accept = select_header_accept(accepts) - !isempty(accept) && (ctx.header["Accept"] = accept) - return nothing +function _header_parameter(value, explode::Bool) + encoded = _encode(value) + encoded isa AbstractDict && return explode ? + _join_object(encoded, ",", "=") : _join_object(encoded, ",", ",") + encoded isa AbstractVector && return _join_array(encoded, ",") + return _scalar(encoded) end -set_header_content_type(ctx::Ctx, ctypes::Vector{T}) where {T} = set_header_content_type(ctx, convert(Vector{String}, ctypes)) -function set_header_content_type(ctx::Ctx, ctypes::Vector{String}) - if !(ctx.method in ("GET", "HEAD")) - ctx.header["Content-Type"] = select_header_content_type(ctypes) +function _cookie_parameter( + name, + value, + style::Symbol, + explode::Bool, + allow_reserved::Bool, +) + style in (:form, :cookie) || + throw(ArgumentError("unsupported cookie parameter style $style")) + encoded = _encode(value) + if style === :form + pairs = _query_parameter(name, value, :form, explode, allow_reserved) + fragment = join( + ( + _escape(key) * "=" * + (preencoded ? item : _escape(item; allow_reserved)) for + (key, item, preencoded) in pairs + ), + '&', + ) + return [(fragment, "", true, true)] + elseif encoded isa AbstractDict + return explode ? + Tuple{String,String,Bool,Bool}[ + (_scalar(key), _scalar(item), true, false) for + (key, item) in _pairs(encoded) + ] : + [(name, _join_object(encoded, ",", ","), true, false)] + elseif encoded isa AbstractVector + return explode ? + Tuple{String,String,Bool,Bool}[ + (name, _scalar(item), true, false) for item in encoded + ] : + [(name, _join_array(encoded, ","), true, false)] end - return nothing + return [(name, _scalar(encoded), true, false)] end -set_param(params::Dict{String,String}, name::String, value::Nothing; collection_format=",", style="form", location=:query, is_explode=default_param_explode(style)) = nothing -# Choose the default collection_format based on spec. -# Overriding it may not match the spec and there's no check. -# But we do not prevent it to allow for wiggle room, since there are many interpretations in the wild over the loosely defined spec around this. -# TODO: `default_param_explode` needs to be improved to handle location too (query, header, cookie...) -function default_param_explode(style::String) - if style == "deepObject" - true - elseif style == "form" - true - else - false +_is_hex_digit(char) = isdigit(char) || 'a' <= lowercase(char) <= 'f' + +function _escape(value; allow_reserved::Bool = false) + text = String(value) + allow_reserved || return HTTP.escapeuri(text) + io = IOBuffer() + index = firstindex(text) + while index <= lastindex(text) + char = text[index] + if char == '%' + second = nextind(text, index) + third = second <= lastindex(text) ? nextind(text, second) : second + if third <= lastindex(text) && + _is_hex_digit(text[second]) && + _is_hex_digit(text[third]) + write(io, char, text[second], text[third]) + index = nextind(text, third) + continue + end + end + if char in ":/?#[]@!\$&'()*+,;=" + write(io, char) + else + write(io, HTTP.escapeuri(string(char))) + end + index = nextind(text, index) end + return String(take!(io)) end -function set_param(params::Dict{String,String}, name::String, value; collection_format=",", style="form", location::Symbol=:query, is_explode=default_param_explode(style)) - deep_explode = style == "deepObject" && is_explode - if deep_explode - merge!(params, deep_object_serialize(Dict(name=>value))) - return nothing + +function _parameter_content_value(client, entry, value) + encoded = _encoded_media_value(client, value, entry[1]) + encoded isa Upload && (encoded = encoded.data) + if encoded isa AbstractVector{UInt8} + isvalid(String, encoded) && return String(copy(encoded)), false + return join( + ('%' * uppercase(string(byte; base = 16, pad = 2)) for byte in encoded), + ), true end - if isa(value, Dict) - # implements the default serialization (style=form, explode=true, location=queryparams) - # as mentioned in https://swagger.io/docs/specification/serialization/ - for (k, v) in value - params[k] = string(v) + return String(encoded), false +end + +function _append_parameter!(client, path, query, headers, cookies, descriptor, value) + location = descriptor.location + style = descriptor.style + explode = descriptor.explode + if location === :querystring + throw(ArgumentError("OAS 3.2 querystring parameter generation is not implemented")) + elseif !isempty(descriptor.content) + serialized, preencoded = + _parameter_content_value(client, first(descriptor.content), value) + if location === :path + path = replace( + path, + "{" * descriptor.name * "}" => + (preencoded ? serialized : _escape(serialized)), + ) + elseif location === :query + push!( + query, + ( + descriptor.name, + serialized, + descriptor.allow_reserved, + preencoded, + ), + ) + elseif location === :header + _set_header!(headers, descriptor.name, serialized) + elseif location === :cookie + push!(cookies, (descriptor.name, serialized, preencoded, false)) + else + throw(ArgumentError("unsupported parameter location $location")) + end + elseif location === :path + serialized = _path_parameter(descriptor.name, value, style, explode) + path = replace(path, "{" * descriptor.name * "}" => serialized) + elseif location === :query + for (name, item, preencoded) in _query_parameter( + descriptor.name, + value, + style, + explode, + descriptor.allow_reserved, + ) + push!(query, (name, item, descriptor.allow_reserved, preencoded)) end - elseif !isa(value, Vector) || isempty(collection_format) - params[name] = string(value) + elseif location === :header + _set_header!(headers, descriptor.name, _header_parameter(value, explode)) + elseif location === :cookie + append!( + cookies, + _cookie_parameter( + descriptor.name, + value, + style, + explode, + descriptor.allow_reserved, + ), + ) + else + throw(ArgumentError("unsupported parameter location $location")) + end + return path +end + +function _safe_header(name, value) + header = String(name) + content = String(value) + occursin(r"^[!#$%&'*+.^_`|~0-9A-Za-z-]+$", header) || + throw(ArgumentError("invalid HTTP header name $(repr(header))")) + (occursin('\r', content) || occursin('\n', content)) && + throw(ArgumentError("HTTP header values must not contain CR or LF")) + return header => content +end + +function _set_header!(headers, name, value) + pair = _safe_header(name, value) + lowered = lowercase(pair.first) + filter!(entry -> lowercase(entry.first) != lowered, headers) + push!(headers, pair) + return headers +end + +_authorizations(credential::AbstractCredential) = credential.authorizations + +function _credential_satisfies(scheme, credential, required) + valid_type = if scheme.type === :apikey + credential isa ApiKeyCredential + elseif scheme.type === :http_basic + credential isa BasicCredential + elseif scheme.type in (:http_bearer, :oauth2, :openidconnect) + credential isa BearerCredential + elseif scheme.type === :mutualtls + credential isa MutualTLSCredential + elseif scheme.type === :http + credential isa HttpCredential else - dlm = get(COLL_DLM, collection_format, ",") - isempty(dlm) && throw(OpenAPIException("Unsupported collection format $collection_format")) - params[name] = join(string.(value), dlm) + false end + valid_type || return false + return all(item -> item in _authorizations(credential), required) end -prep_args(ctx::Ctx) = prep_args(Val(ctx.client.httplib), ctx) +function _security!(client, requirements, query, headers, cookies, base_options) + isempty(requirements) && return base_options + for requirement in requirements + isempty(requirement) && return base_options + all(requirement) do entry + credential = get(client.credentials, entry[1], nothing) + credential === nothing && return false + scheme = get(_SECURITY_SCHEMES, entry[1], nothing) + return scheme !== nothing && + _credential_satisfies(scheme, credential, entry[2]) + end || continue + options = base_options + for (name, _) in requirement + scheme = _SECURITY_SCHEMES[name] + credential = client.credentials[name] + if scheme.type === :apikey + credential isa ApiKeyCredential || + throw(ArgumentError("security scheme $name requires ApiKeyCredential")) + if scheme.location === :header + _set_header!(headers, scheme.name, credential.value) + elseif scheme.location === :query + push!(query, (scheme.name, credential.value, false, false)) + elseif scheme.location === :cookie + push!(cookies, (scheme.name, credential.value, false, false)) + end + elseif scheme.type === :http_basic + credential isa BasicCredential || + throw(ArgumentError("security scheme $name requires BasicCredential")) + token = Base64.base64encode(credential.username * ":" * credential.password) + _set_header!(headers, "Authorization", "Basic " * token) + elseif scheme.type in (:http_bearer, :oauth2, :openidconnect) + credential isa BearerCredential || + throw(ArgumentError("security scheme $name requires BearerCredential")) + _set_header!(headers, "Authorization", "Bearer " * credential.token) + elseif scheme.type === :mutualtls + credential isa MutualTLSCredential || + throw(ArgumentError("security scheme $name requires MutualTLSCredential")) + options = merge(options, credential.request_options) + elseif scheme.type === :http + credential isa HttpCredential || + throw(ArgumentError("security scheme $name requires HttpCredential")) + _set_header!( + headers, + "Authorization", + scheme.scheme * " " * credential.value, + ) + else + throw(ArgumentError("security scheme type $(scheme.type) is not supported")) + end + end + return options + end + names = join((join(first.(requirement), " + ") for requirement in requirements), " or ") + client.require_credentials && throw( + ArgumentError("no configured credentials satisfy operation security: " * names), + ) + return base_options +end + +function _media_match_score(received::String, documented::String) + received == documented && return 4 + documented == "*/*" && return 1 + parts = split(documented, '/'; limit = 2) + received_parts = split(received, '/'; limit = 2) + length(parts) == 2 && length(received_parts) == 2 || return 0 + parts[1] == received_parts[1] || parts[1] == "*" || return 0 + parts[2] == "*" && return parts[1] == "*" ? 1 : 2 + startswith(parts[2], "*+") && + endswith(received_parts[2], parts[2][2:end]) && return 3 + return 0 +end -response(::Type{Nothing}, resp::HTTPLibResponse, body) = nothing::Nothing -response(::Type{T}, resp::HTTPLibResponse, body) where {T <: Real} = response(T, body)::T -response(::Type{T}, resp::HTTPLibResponse, body) where {T <: String} = response(T, body)::T -function response(::Type{T}, resp::HTTPLibResponse, body) where {T} - ctype = get_response_header(resp, "Content-Type", "application/json") - response(T, is_json_mime(ctype), body)::T +_media_match(received::String, documented::String) = + _media_match_score(_base_media_type(received), _base_media_type(documented)) > 0 + +_base_media_type(value) = lowercase(strip(first(split(String(value), ';'; limit = 2)))) + +function _select_media(media, received) + isempty(media) && return nothing + normalized = _base_media_type(received) + selected = nothing + score = 0 + for entry in media + candidate = _media_match_score(normalized, _base_media_type(entry[1])) + if candidate > score + selected = entry + score = candidate + end + end + return selected end -response(::Type{T}, ::Nothing, body) where {T} = response(T, true, body) -function response(::Type{T}, is_json::Bool, body) where {T} - (length(body) == 0) && return T() - response(T, is_json ? JSON.parse(String(body)) : body)::T + +function _select_response(responses, status) + exact = string(status) + range = string(div(status, 100), "XX") + default = nothing + for response in responses + selector = uppercase(response.selector) + selector == exact && return response + selector == range && (default = response) + selector == "DEFAULT" && default === nothing && (default = response) + end + return default end -response(::Type{String}, data::Vector{UInt8}) = String(data) -response(::Type{T}, data::Vector{UInt8}) where {T<:Real} = parse(T, String(data)) -response(::Type{T}, data::T) where {T} = data -response(::Type{ZonedDateTime}, data) = str2zoneddatetime(data) -response(::Type{DateTime}, data) = str2datetime(data) -response(::Type{Date}, data) = str2date(data) +function _header_values(headers, name) + lowered = lowercase(String(name)) + return String[ + value for (key, value) in headers if lowercase(String(key)) == lowered + ] +end -response(::Type{T}, data) where {T} = convert(T, data) -response(::Type{T}, data::AbstractDict{String,Any}) where {T} = from_json(T, data)::T -response(::Type{T}, data::AbstractDict{String,Any}) where {T<:Dict} = convert(T, Dict{String,Any}(data)) -response(::Type{Vector{T}}, data::Vector{V}) where {T,V} = T[response(T, v) for v in data] +function _header_atom(value) + text = String(value) + lowered = lowercase(text) + lowered == "true" && return true + lowered == "false" && return false + lowered == "null" && return nothing + integer = tryparse(Int64, text) + integer === nothing || return integer + number = tryparse(Float64, text) + number === nothing || return number + return text +end -noop_pre_request_hook(ctx::Ctx) = ctx -noop_pre_request_hook(resource_path::AbstractString, body::Any, headers::Dict{String,String}) = (resource_path, body, headers) +function _header_scalar(type, value) + text = String(value) + type === String && return text + type === Bool && return lowercase(text) == "true" ? true : + lowercase(text) == "false" ? false : + throw(DecodeError("invalid boolean response header $(repr(text))")) + type <: Integer && return try + parse(type, text) + catch error + throw(DecodeError("invalid integer response header: $(sprint(showerror, error))")) + end + type <: AbstractFloat && return try + parse(type, text) + catch error + throw(DecodeError("invalid numeric response header: $(sprint(showerror, error))")) + end + return _decode(type, text) +end -function do_request(ctx::Ctx, stream::Bool=false; stream_to::Union{Channel,Nothing}=nothing) - # call the user hook to allow them to modify the request context - ctx = ctx.pre_request_hook(ctx) +function _header_type_variant(type, shape) + variants = type isa Union ? Base.uniontypes(type) : (type,) + for variant in variants + variant in (Nothing, Absent) && continue + shape === :array && variant <: AbstractVector && return variant + shape === :object && + (variant <: AbstractDict || isstructtype(variant)) && return variant + shape === :scalar && return variant + end + return type +end - # prepare the url - resource_path = replace(ctx.resource, "{format}"=>"json") - for (k,v) in ctx.path - esc_v = ctx.escape_path_params ? escapeuri(v) : v - resource_path = replace(resource_path, "{$k}"=>esc_v) +function _decode_schema_header( + type, + values, + shape, + explode, + schema; + set_cookie::Bool = false, +) + selected = _header_type_variant(type, shape) + raw = if shape === :array + items = if set_cookie + String[String(value) for value in values] + else + output = String[] + for value in values + append!(output, split(value, ',')) + end + output + end + element = selected <: AbstractVector ? eltype(selected) : String + Any[_header_scalar(element, item) for item in items] + elseif shape === :object + object = JSON.Object{String,Any}() + if set_cookie && explode + for value in values + pair = split(value, '='; limit = 2) + length(pair) == 2 || throw( + DecodeError("invalid Set-Cookie response header"), + ) + object[pair[1]] = pair[2] + end + elseif explode + tokens = split(join(values, ','), ',') + for token in tokens + pair = split(token, '='; limit = 2) + length(pair) == 2 || throw( + DecodeError("invalid exploded object response header"), + ) + object[pair[1]] = _header_atom(pair[2]) + end + else + tokens = split(join(values, ','), ',') + iseven(length(tokens)) || + throw(DecodeError("invalid object response header")) + for index in 1:2:length(tokens) + object[tokens[index]] = _header_atom(tokens[index + 1]) + end + end + object + else + _header_scalar(selected, join(values, set_cookie ? '\n' : ',')) end - # append query params if needed - if !isempty(ctx.query) - resource_path = string(URIs.URI(URIs.URI(resource_path); query=escapeuri(ctx.query))) + _validate_schema( + schema, + _encode(raw), + "decoding a response header"; + direction = :output, + ) + return _decode(type, raw) +end + +function _decode_response_headers(client, descriptor, headers) + output = Dict{String,Any}() + descriptor === nothing && return output + for header in descriptor.headers + values = _header_values(headers, header.name) + if isempty(values) + header.required && throw( + DecodeError("required response header $(repr(header.name)) is missing"), + ) + continue + end + decoded = if isempty(header.content) + _decode_schema_header( + header.type, + values, + header.shape, + header.explode, + client.validate_responses ? header.schema : nothing, + set_cookie = lowercase(header.name) == "set-cookie", + ) + else + entry = first(header.content) + separator = lowercase(header.name) == "set-cookie" ? '\n' : ',' + _decode_body( + client, + header.type, + entry[1], + Vector{UInt8}(codeunits(join(values, separator))), + entry[3], + ) + end + output[header.name] = decoded end + return output +end - body, kwargs = prep_args(ctx) +_is_json_media(media) = media == "application/json" || endswith(media, "+json") +_is_sequential_json_media(media) = media in ( + "application/jsonl", + "application/x-ndjson", + "application/json-seq", + "application/geo+json-seq", +) || endswith(media, "+json-seq") + +function _parse_json(text, context) + source = text isa AbstractString ? String(text) : String(copy(text)) + isvalid(source) || throw(DecodeError("invalid UTF-8 while $context")) + try + return JSON.parse(source; duplicate_keys = :error) + catch error + throw(DecodeError("invalid JSON while $context: $(sprint(showerror, error))")) + end +end - # call the user hook again, to allow them to modify the processed request - resource_path, body, headers = ctx.pre_request_hook(resource_path, body, kwargs[:headers]) - kwargs[:headers] = headers +function _decode_sequential_json(body, media) + records = Any[] + if media == "application/json-seq" || endswith(media, "+json-seq") + for chunk in split(String(body), Char(0x1e)) + text = strip(chunk) + isempty(text) || push!(records, _parse_json(text, "decoding a JSON sequence")) + end + else + for line in eachline(IOBuffer(body)) + text = strip(line) + isempty(text) || push!(records, _parse_json(text, "decoding JSON lines")) + end + end + return records +end - if stream - @assert stream_to !== nothing +function _decode_body(client::Client, type, content_type, body, schema) + media = _base_media_type(content_type) + decoder = get(client.media_decoders, media, nothing) + if decoder !== nothing + value = decoder(copy(body), String(content_type)) + lowered = _encode(value) + client.validate_responses && + _validate_schema( + schema, + lowered, + "decoding a custom-media response"; + direction = :output, + ) + return _decode(type, lowered) + elseif _is_json_media(media) + if isempty(body) + client.validate_responses && + _validate_schema( + schema, + nothing, + "decoding an empty JSON response"; + direction = :output, + ) + return _decode(type, nothing) + end + value = _parse_json(body, "decoding a response") + client.validate_responses && + _validate_schema( + schema, + value, + "decoding a JSON response"; + direction = :output, + ) + return _decode(type, value) + elseif _is_sequential_json_media(media) + value = _decode_sequential_json(body, media) + client.validate_responses && + _validate_schema( + schema, + value, + "decoding a sequential JSON response"; + direction = :output, + ) + return _decode(type, value) + elseif startswith(media, "text/") || type === String + isvalid(String, body) || throw(DecodeError("response text is not UTF-8")) + value = String(copy(body)) + client.validate_responses && + _validate_schema( + schema, + value, + "decoding a text response"; + direction = :output, + ) + return _decode(type, value) + elseif type === Vector{UInt8} || type === Any || media == "application/octet-stream" + client.validate_responses && _validate_schema( + schema, + Base64.base64encode(body), + "decoding a binary response", + ; direction = :output, + ) + return type === Any ? copy(body) : _decode(type, body) end + throw(UnsupportedMediaType(String(content_type), :response)) +end - output = Base.BufferStream() - resp, output = do_request(Val(ctx.client.httplib), ctx, resource_path, body, output, kwargs, stream; stream_to=stream_to) +_form_fields(value::AbstractDict) = Pair{String,Any}[ + String(key) => item for (key, item) in value +] +_form_fields(value::NamedTuple) = Pair{String,Any}[ + String(key) => item for (key, item) in pairs(value) +] +function _form_fields(value) + encoded = _encode(value) + encoded isa AbstractDict || + throw(ArgumentError("form request body must encode as an object")) + return Pair{String,Any}[String(key) => item for (key, item) in encoded] +end - return resp, output +function _body_bytes(value, media_type) + value isa Upload && return copy(value.data) + value isa AbstractVector{UInt8} && return Vector{UInt8}(value) + value isa AbstractString && return Vector{UInt8}(codeunits(value)) + throw( + ArgumentError( + "codec for $(repr(media_type)) must return a string, byte vector, or Upload", + ), + ) end -function exec(ctx::Ctx, stream_to::Union{Channel,Nothing}=nothing) - stream = stream_to !== nothing - resp, output = do_request(ctx, stream; stream_to=stream_to) +function _encoded_media_value(client, value, media_type) + media = _base_media_type(media_type) + if _is_json_media(media) + return JSON.json(_encode(value)) + elseif startswith(media, "text/") + return _scalar(value) + end + encoder = get(client.media_encoders, media, nothing) + encoder === nothing || return encoder(value, String(media_type)) + value isa Upload && return value.data + value isa AbstractVector{UInt8} && return value + value isa AbstractString && return String(value) + throw(UnsupportedMediaType(String(media_type), :request)) +end - if resp === nothing - # request was interrupted - throw(InvocationException("request was interrupted")) +function _configured_content_type(configured, preferred = nothing) + configured === nothing && return preferred + options = String[ + strip(item) for item in split(String(configured), ',') if !isempty(strip(item)) + ] + isempty(options) && throw(ArgumentError("encoding contentType is empty")) + if preferred !== nothing + any(option -> _media_match(String(preferred), option), options) || throw( + ArgumentError( + "part Content-Type $(repr(preferred)) is not allowed by encoding contentType $(repr(configured))", + ), + ) + return String(preferred) end + return first(options) +end - if isa(resp, HTTPLibError) - throw(ApiException(resp)) +function _form_value(client, value, configured_type = nothing) + if configured_type !== nothing + selected_type = _configured_content_type(configured_type) + encoded = _encoded_media_value(client, value, selected_type) + encoded isa Upload && return Base64.base64encode(encoded.data) + encoded isa AbstractVector{UInt8} && return Base64.base64encode(encoded) + return String(encoded) end + value isa Upload && return Base64.base64encode(value.data) + value isa AbstractVector{UInt8} && return Base64.base64encode(value) + value isa AbstractDict && return JSON.json(_encode(value)) + value isa NamedTuple && return JSON.json(_encode(value)) + encoded = _encode(value) + encoded isa AbstractDict && return JSON.json(encoded) + return _scalar(encoded) +end - if stream - return stream_to, ApiResponse(resp) - else - data = read(output) - return_type = ctx.client.get_return_type(ctx.return_types, resp.status, String(copy(data))) - if isnothing(return_type) - return nothing, ApiResponse(resp) +function _form_pairs(client, value, encodings) + output = Tuple{String,String,Bool,Bool}[] + configured = Dict(encoding.name => encoding for encoding in encodings) + for (key, item) in _form_fields(value) + encoding = get(configured, key, nothing) + if encoding !== nothing && encoding.rfc6570 + style = something(encoding.style, :form) + explode = something(encoding.explode, style === :form) + allow_reserved = encoding.allow_reserved + for (name, encoded, preencoded) in _query_parameter( + key, + item, + style, + explode, + allow_reserved, + ) + push!( + output, + (name, encoded, allow_reserved, preencoded), + ) + end + continue + end + configured_type = encoding === nothing ? nothing : encoding.content_type + if item isa AbstractVector && !(item isa AbstractVector{UInt8}) + for child in item + push!( + output, + (String(key), _form_value(client, child, configured_type), false, false), + ) + end + else + push!( + output, + (String(key), _form_value(client, item, configured_type), false, false), + ) end - return response(return_type, resp, data), ApiResponse(resp) end + return output end -function setproperty!(o::T, name::Symbol, val) where {T<:APIModel} - validate_property(T, name, val) - fieldtype = property_type(T, name) +_form_escape(value) = replace(_escape(value), "%20" => "+") - if isa(val, fieldtype) - return setfield!(o, name, val) - elseif fieldtype === ZonedDateTime - return setfield!(o, name, str2zoneddatetime(val)) - elseif fieldtype === DateTime - return setfield!(o, name, str2datetime(val)) - elseif fieldtype === Date - return setfield!(o, name, str2date(val)) - else - ftval = try - convert(fieldtype, val) - catch - fieldtype(val) +function _safe_multipart_text(value, label) + text = String(value) + (occursin('\r', text) || occursin('\n', text)) && + throw(ArgumentError("multipart $label must not contain CR or LF")) + return text +end + +function _multipart_quoted_text(value, label) + text = _safe_multipart_text(value, label) + return replace( + text, + Char(92) => string(Char(92), Char(92)), + Char(34) => string(Char(92), Char(34)), + ) +end + +function _multipart_content( + client, + value, + configured_type, + nested_encodings, + nested_headers, +) + has_nested_headers = !isempty( + _string_keyed_map(nested_headers, "nested multipart_headers"), + ) + if !isempty(nested_encodings) && configured_type === nothing + throw( + ArgumentError( + "nested Encoding Objects require an explicit multipart or form contentType", + ), + ) + end + if value isa Upload || value isa AbstractVector{UInt8} + (isempty(nested_encodings) && !has_nested_headers) || throw( + ArgumentError("binary multipart values cannot contain nested encodings"), + ) + end + if value isa Upload + content_type = something( + _configured_content_type(configured_type, value.content_type), + "application/octet-stream", + ) + return value.data, value.filename, content_type, value.headers + elseif value isa AbstractVector{UInt8} + return Vector{UInt8}(value), nothing, + something( + _configured_content_type(configured_type), + "application/octet-stream", + ), + Pair{String,String}[] + elseif configured_type !== nothing + selected_type = _configured_content_type(configured_type) + if !isempty(nested_encodings) + encoded, actual_type = _encode_body( + client, + value, + selected_type, + nested_encodings; + multipart_headers = nested_headers, + ) + return _body_bytes(encoded, actual_type), nothing, actual_type, + Pair{String,String}[] end - return setfield!(o, name, ftval) + has_nested_headers && throw( + ArgumentError("nested multipart headers require nested Encoding Objects"), + ) + encoded = _encoded_media_value(client, value, selected_type) + return _body_bytes(encoded, selected_type), nothing, selected_type, + Pair{String,String}[] + elseif value isa AbstractString || value isa Number || value isa Bool + has_nested_headers && throw( + ArgumentError("nested multipart headers require nested Encoding Objects"), + ) + return Vector{UInt8}(codeunits(_scalar(value))), nothing, + something(configured_type, "text/plain"), + Pair{String,String}[] end + has_nested_headers && throw( + ArgumentError("nested multipart headers require nested Encoding Objects"), + ) + return Vector{UInt8}(codeunits(JSON.json(_encode(value)))), nothing, + something(configured_type, "application/json"), + Pair{String,String}[] end -""" - getpropertyat(o::T, path...) where {T<:APIModel} - -Returns the property at the specified path. -The path can be a single property name or a chain of property names separated by dots, representing a nested property. -""" -function getpropertyat(o::T, path...) where {T<:APIModel} - val = getproperty(o, Symbol(path[1])) - rempath = path[2:end] - (length(rempath) == 0) && (return val) - - if isa(val, Vector) - if isa(rempath[1], Integer) - val = val[rempath[1]] - rempath = rempath[2:end] - else - return [getpropertyat(item, rempath...) for item in val] +function _multipart_style_pairs(name, value, style, explode) + encoded = _encode(value) + if style === :deepObject + encoded isa AbstractDict || throw(ArgumentError("deepObject requires an object")) + return Pair{String,Any}[ + string(name, '[', key, ']') => item for (key, item) in _pairs(encoded) + ] + elseif style in (:spaceDelimited, :pipeDelimited) + explode && throw(ArgumentError("$style with explode=true is undefined")) + delimiter = style === :spaceDelimited ? " " : "|" + return [String(name) => (encoded isa AbstractDict ? + _join_object(encoded, delimiter, delimiter) : + _join_array(encoded, delimiter))] + elseif style === :form + if encoded isa AbstractDict + return explode ? + Pair{String,Any}[String(key) => item for (key, item) in _pairs(encoded)] : + [String(name) => _join_object(encoded, ",", ",")] + elseif encoded isa AbstractVector + return explode ? [String(name) => item for item in encoded] : + [String(name) => _join_array(encoded, ",")] end + return [String(name) => encoded] end + throw(ArgumentError("unsupported multipart parameter style $style")) +end - (length(rempath) == 0) && (return val) - getpropertyat(val, rempath...) +function _string_keyed_map(value, label) + value isa AbstractDict || value isa NamedTuple || throw( + ArgumentError("$label must be a dictionary or named tuple"), + ) + output = Dict{String,Any}() + for (key, item) in pairs(value) + name = String(key) + haskey(output, name) && throw( + ArgumentError("$label contains duplicate key $(repr(name))"), + ) + output[name] = item + end + return output end -""" - haspropertyat(o::T, path...) where {T<:APIModel} +function _multipart_encoding_headers(client, encoding, supplied) + supplied_map = _string_keyed_map(supplied, "multipart part headers") + documented = Dict(lowercase(header.name) => header for header in encoding.headers) + values = Dict{String,Any}() + for (name, value) in supplied_map + lowered = lowercase(name) + haskey(values, lowered) && throw( + ArgumentError( + "multipart part headers contain duplicate case-insensitive name $(repr(name))", + ), + ) + descriptor = get(documented, lowered, nothing) + descriptor === nothing && throw( + ArgumentError( + "multipart header $(repr(name)) is not documented for part $(repr(encoding.name))", + ), + ) + values[lowered] = value + end -Returns true if the supplied object has the property at the specified path. -""" -function haspropertyat(o::T, path...) where {T<:APIModel} - p1 = Symbol(path[1]) - ret = hasproperty(o, p1) - rempath = path[2:end] - (length(rempath) == 0) && (return ret) - ret || (return false) - - val = getproperty(o, p1) - if isa(val, Vector) - if isa(rempath[1], Integer) - ret = length(val) >= rempath[1] - if ret - val = val[rempath[1]] - rempath = rempath[2:end] - end + output = Pair{String,String}[] + for descriptor in encoding.headers + lowered = lowercase(descriptor.name) + if !haskey(values, lowered) + descriptor.required && throw( + ArgumentError( + "required multipart header $(repr(descriptor.name)) is missing for part $(repr(encoding.name))", + ), + ) + continue + end + value = values[lowered] + _validate_schema( + descriptor.schema, + _encode(value), + "encoding multipart header $(descriptor.name)"; + direction = :input, + ) + serialized = if isempty(descriptor.content) + _header_parameter(value, descriptor.explode) else - return [haspropertyat(item, rempath...) for item in val] + text, binary = _parameter_content_value( + client, + first(descriptor.content), + value, + ) + binary && throw( + ArgumentError( + "multipart header content for $(repr(descriptor.name)) must encode as text", + ), + ) + text end + push!(output, _safe_header(descriptor.name, serialized)) end + return output +end - (length(rempath) == 0) && (return ret) - haspropertyat(val, rempath...) +function _multipart_part_headers(value) + value isa MultipartPartHeaders && return value.values, value.parts + return value, NamedTuple() end -Base.hasproperty(o::T, name::Symbol) where {T<:APIModel} = ((name in propertynames(o)) && (getproperty(o, name) !== nothing)) +function _merge_multipart_headers(upload_headers, encoding_headers) + output = Pair{String,String}[] + seen = Set{String}() + for (name, value) in Iterators.flatten((upload_headers, encoding_headers)) + lowered = lowercase(String(name)) + lowered in ("content-type", "content-disposition") && throw( + ArgumentError( + "multipart part headers must not override Content-Type or Content-Disposition", + ), + ) + lowered in seen && throw( + ArgumentError( + "multipart part contains duplicate case-insensitive header $(repr(name))", + ), + ) + push!(seen, lowered) + push!(output, _safe_header(name, value)) + end + return output +end -convert(::Type{T}, json::AbstractDict{String,Any}) where {T<:APIModel} = from_json(T, json) -convert(::Type{T}, v::Nothing) where {T<:APIModel} = T() -convert(::Type{T}, v::T) where {T<:OneOfAPIModel} = v -convert(::Type{T}, json::AbstractDict{String,Any}) where {T<:OneOfAPIModel} = from_json(T, json) -convert(::Type{T}, v) where {T<:OneOfAPIModel} = T(v) -convert(::Type{T}, v::String) where {T<:OneOfAPIModel} = T(v) -convert(::Type{T}, v::T) where {T<:AnyOfAPIModel} = v -convert(::Type{T}, json::AbstractDict{String,Any}) where {T<:AnyOfAPIModel} = from_json(T, json) -convert(::Type{T}, v) where {T<:AnyOfAPIModel} = T(v) -convert(::Type{T}, v::String) where {T<:AnyOfAPIModel} = T(v) +function _multipart_body( + client, + value, + media_type, + encodings, + multipart_headers, +) + boundary = replace(string(UUIDs.uuid4()), "-" => "") + configured = Dict(encoding.name => encoding for encoding in encodings) + supplied = _string_keyed_map(multipart_headers, "multipart_headers") + used_supplied = Set{String}() + io = IOBuffer() + for (name, raw_item) in _form_fields(value) + encoding = get(configured, name, nothing) + fields = if encoding !== nothing && encoding.rfc6570 + _multipart_style_pairs( + name, + raw_item, + something(encoding.style, :form), + something(encoding.explode, true), + ) + else + items = raw_item isa AbstractVector && + !(raw_item isa AbstractVector{UInt8}) ? raw_item : (raw_item,) + Pair{String,Any}[String(name) => item for item in items] + end + configured_type = encoding === nothing || encoding.rfc6570 ? nothing : + encoding.content_type + supplied_configuration = get(supplied, name, NamedTuple()) + haskey(supplied, name) && push!(used_supplied, name) + supplied_headers, nested_headers = + _multipart_part_headers(supplied_configuration) + declared_headers = if encoding === nothing + isempty(_string_keyed_map(supplied_headers, "multipart part headers")) || throw( + ArgumentError( + "multipart headers were supplied for undocumented part $(repr(name))", + ), + ) + Pair{String,String}[] + else + _multipart_encoding_headers(client, encoding, supplied_headers) + end + nested_encodings = encoding === nothing ? () : encoding.encoding + if encoding === nothing && + !isempty(_string_keyed_map(nested_headers, "nested multipart_headers")) + throw( + ArgumentError( + "nested multipart headers were supplied for undocumented part $(repr(name))", + ), + ) + end + for (part_name, item) in fields + content, filename, content_type, upload_headers = + _multipart_content( + client, + item, + configured_type, + nested_encodings, + nested_headers, + ) + headers = _merge_multipart_headers(upload_headers, declared_headers) + safe_name = _multipart_quoted_text(part_name, "part name") + write(io, "--", boundary, "\r\n") + write(io, "Content-Disposition: form-data; name=", Char(34), safe_name, Char(34)) + if filename !== nothing + safe_filename = _multipart_quoted_text(filename, "filename") + write(io, "; filename=", Char(34), safe_filename, Char(34)) + end + write(io, "\r\nContent-Type: ", _safe_multipart_text(content_type, "content type"), "\r\n") + for (header, header_value) in headers + write( + io, + _safe_multipart_text(header, "header name"), + ": ", + _safe_multipart_text(header_value, "header value"), + "\r\n", + ) + end + write(io, "\r\n", content, "\r\n") + end + end + unused = setdiff(Set(keys(supplied)), used_supplied) + isempty(unused) || throw( + ArgumentError( + "multipart headers were supplied for absent parts: " * + join(repr.(sort!(collect(unused))), ", "), + ), + ) + write(io, "--", boundary, "--\r\n") + return take!(io), String(media_type) * "; boundary=" * boundary +end -show(io::IO, model::T) where {T<:UnionAPIModel} = print(io, JSON.json(model.value, 2)) -show(io::IO, model::T) where {T<:APIModel} = print(io, JSON.json(model, 2)) -summary(io::IO, model::T) where {T<:APIModel} = print(io, T) +function _encode_sequential_json(value, media) + encoded = _encode(value) + encoded isa AbstractVector || encoded isa Tuple || throw( + ArgumentError("sequential JSON request bodies must be arrays or tuples"), + ) + if media == "application/json-seq" || endswith(media, "+json-seq") + return join((string(Char(0x1e), JSON.json(item), '\n') for item in encoded)) + end + return join((JSON.json(item) * "\n" for item in encoded)) +end -""" - is_longpoll_timeout(ex::Exception) +function _encode_body( + client, + value, + media_type, + encodings; + multipart_headers = NamedTuple(), +) + media = _base_media_type(media_type) + if !startswith(media, "multipart/") + isempty(_string_keyed_map(multipart_headers, "multipart_headers")) || throw( + ArgumentError( + "multipart_headers can only be used with a multipart request body", + ), + ) + end + if _is_json_media(media) + return JSON.json(_encode(value)), media_type + elseif _is_sequential_json_media(media) + return _encode_sequential_json(value, media), media_type + elseif media == "application/x-www-form-urlencoded" + pairs = _form_pairs(client, value, encodings) + body = join( + ( + _form_escape(key) * "=" * + (preencoded ? item : replace( + _escape(item; allow_reserved), + "%20" => "+", + )) for (key, item, allow_reserved, preencoded) in pairs + ), + '&', + ) + return body, media_type + elseif startswith(media, "multipart/") + return _multipart_body( + client, + value, + media_type, + encodings, + multipart_headers, + ) + elseif startswith(media, "text/") + return value isa AbstractString ? String(value) : _scalar(value), media_type + end + return _body_bytes(_encoded_media_value(client, value, media_type), media_type), media_type +end -Examine the supplied exception and return true if the reason is timeout -of a long polling request. If the exception is a nested exception of type -CompositeException or TaskFailedException, then navigates through the nested -exception values to examine the leaves. -""" -is_longpoll_timeout(ex) = false -is_longpoll_timeout(ex::TaskFailedException) = is_longpoll_timeout(ex.task.exception) -is_longpoll_timeout(ex::CompositeException) = any(is_longpoll_timeout, ex.exceptions) -function is_longpoll_timeout(ex::ApiException) - # All client library wrappers ensure that the reason string format is the same for longpoll timeouts - ex.status == 200 && match(r"Operation timed out after \d+ milliseconds with \d+ bytes received", ex.reason) !== nothing +function _server_for(client::Client, operation) + if client.server !== nothing + return String(rstrip(client.server, '/')) + end + if client === DEFAULT_CLIENT && SERVER[] != _DEFAULT_SERVER + return String(rstrip(SERVER[], '/')) + end + servers = operation.servers + isempty(servers) && return _DEFAULT_SERVER + selected = if client.server_name === nothing + client.server_index <= length(servers) || throw( + ArgumentError( + "server index $(client.server_index) exceeds the $(length(servers)) documented servers for $(operation.id)", + ), + ) + servers[client.server_index] + else + index = findfirst(server -> server.name == client.server_name, servers) + index === nothing && throw( + ArgumentError( + "server name $(repr(client.server_name)) is not documented for $(operation.id)", + ), + ) + servers[index] + end + url = selected.url + for variable in selected.variables + value = get(client.server_variables, variable.name, variable.default) + isempty(variable.values) || value in variable.values || throw( + ArgumentError( + "server variable $(repr(variable.name)) value $(repr(value)) is not in its documented enum", + ), + ) + url = replace(url, "{" * variable.name * "}" => value) + end + occursin('{', url) && throw( + ArgumentError("not all server URL variables were supplied for $(operation.id)"), + ) + if startswith(lowercase(url), "http://") || startswith(lowercase(url), "https://") + return String(rstrip(url, '/')) + end + base = selected.base + if startswith(lowercase(base), "http://") || startswith(lowercase(base), "https://") + resolved = SchemaEngine.Resources.URIs.resolvereference( + SchemaEngine.Resources.URIs.URI(base), + SchemaEngine.Resources.URIs.URI(url), + ) + return String(rstrip(string(resolved), '/')) + end + return String(rstrip(_DEFAULT_SERVER * (startswith(url, '/') ? url : "/" * url), '/')) end +function _request( + client::Client, + operation, + values::Dict{Symbol,Any}; + body = ABSENT, + content_type = nothing, + accept = nothing, + with_http_info::Bool = false, + request_headers = Pair{String,String}[], + request_options::NamedTuple = NamedTuple(), + multipart_headers = NamedTuple(), +) + path = operation.path + query = Tuple{String,String,Bool,Bool}[] + headers = Pair{String,String}[_safe_header(key, value) for (key, value) in client.headers] + for (key, value) in request_headers + _set_header!(headers, key, value) + end + cookies = Tuple{String,String,Bool,Bool}[] + for descriptor in operation.parameters + value = get(values, descriptor.arg, ABSENT) + if value isa Absent + descriptor.required && + throw(ArgumentError("required parameter $(descriptor.name) is missing")) + continue + end + client.validate_requests && _validate_schema( + descriptor.schema, + _encode(value), + "encoding parameter $(descriptor.name)"; + direction = :input, + ) + path = _append_parameter!( + client, + path, + query, + headers, + cookies, + descriptor, + value, + ) + end + occursin('{', path) && + throw(ArgumentError("not all path template parameters were supplied for $(operation.id)")) + base_options = merge(client.request_options, request_options) + options = _security!( + client, + operation.security, + query, + headers, + cookies, + base_options, + ) + if !isempty(cookies) + cookie_text = join( + ( + fragment ? key : + (raw ? key : _escape(key)) * "=" * + (raw ? value : _escape(value)) for + (key, value, raw, fragment) in cookies + ), + "; ", + ) + existing = join(_header_values(headers, "Cookie"), "; ") + _set_header!( + headers, + "Cookie", + isempty(existing) ? cookie_text : existing * "; " * cookie_text, + ) + options = merge(options, (; cookies = false)) + end + query_text = isempty(query) ? "" : "?" * join( + ( + _escape(key) * "=" * + (preencoded ? value : _escape(value; allow_reserved)) for + (key, value, allow_reserved, preencoded) in query + ), + '&', + ) + url = _server_for(client, operation) * + (startswith(path, '/') ? path : "/" * path) * query_text + + payload = UInt8[] + if operation.request !== nothing && !(body isa Absent) + media = operation.request.media + selected = content_type === nothing ? first(media) : + something(findfirst(entry -> _media_match(lowercase(content_type), lowercase(entry[1])), media), 0) + selected === 0 && throw( + ArgumentError("unsupported request Content-Type $(repr(content_type)) for $(operation.id)"), + ) + entry = selected isa Integer ? media[selected] : selected + client.validate_requests && _validate_schema( + entry[3], + _encode(body), + "encoding the $(operation.id) request body"; + direction = :input, + ) + payload, actual_content_type = + _encode_body( + client, + body, + entry[1], + entry[4]; + multipart_headers, + ) + _set_header!(headers, "Content-Type", actual_content_type) + elseif operation.request !== nothing && operation.request.required + throw(ArgumentError("required request body is missing for $(operation.id)")) + end + documented_accept = String[ + entry[1] for response in operation.responses for entry in response.media + if startswith(uppercase(response.selector), "2") || + uppercase(response.selector) == "DEFAULT" + ] + existing_accept = _header_values(headers, "Accept") + selected_accept = if accept !== nothing + String(accept) + elseif !isempty(existing_accept) + nothing + else + join(unique(documented_accept), ", ") + end + selected_accept === nothing || isempty(selected_accept) || + _set_header!(headers, "Accept", selected_accept) + + options = merge(options, (body = payload, status_exception = false)) + response = HTTP.request( + operation.method, + url, + headers; + options..., + ) + response_headers = Pair{String,String}[ + String(key) => String(value) for (key, value) in response.headers + ] + bytes = Vector{UInt8}(response.body) + descriptor = _select_response(operation.responses, response.status) + received = HTTP.header(response, "Content-Type", "") + decoded = nothing + decoded_headers = Dict{String,Any}() + decode_error = nothing + try + decoded_headers = _decode_response_headers(client, descriptor, response_headers) + if descriptor !== nothing + if isempty(descriptor.media) + if !isempty(bytes) && 200 <= response.status < 300 + throw(UnexpectedBody(operation.id, response.status, length(bytes))) + end + decoded = nothing + else + selected = isempty(received) && isempty(bytes) ? first(descriptor.media) : + _select_media(descriptor.media, received) + selected === nothing && throw( + UnexpectedContentType( + operation.id, + response.status, + received, + Tuple(first.(descriptor.media)), + ), + ) + actual_media = isempty(received) ? selected[1] : received + decoded = _decode_body( + client, + selected[2], + actual_media, + bytes, + selected[3], + ) + end + elseif isempty(operation.responses) + decoded = isempty(bytes) ? nothing : copy(bytes) + end + catch error + 200 <= response.status < 300 && rethrow() + decode_error = error + end + if !(200 <= response.status < 300) + throw( + ApiError( + operation.id, + response.status, + response_headers, + decoded_headers, + bytes, + decoded, + decode_error, + ), + ) + end + descriptor === nothing && !isempty(operation.responses) && throw( + ApiError( + operation.id, + response.status, + response_headers, + decoded_headers, + bytes, + nothing, + nothing, + ) + ) + return with_http_info ? + ApiResponse(response.status, response_headers, decoded_headers, decoded) : + decoded +end """ - is_request_interrupted(ex::Exception) -Examine the supplied exception and return true if the reason is that the -request was interrupted. If the exception is a nested exception of type -CompositeException or TaskFailedException, then navigates through the nested -exception values to examine the leaves. -""" -is_request_interrupted(ex) = false -is_request_interrupted(ex::TaskFailedException) = is_request_interrupted(ex.task.exception) -is_request_interrupted(ex::CompositeException) = any(is_request_interrupted, ex.exceptions) -is_request_interrupted(ex::InvocationException) = ex.reason == "request was interrupted" +function _julia_literal(value) + Base.@nospecialize value + value === nothing && return "nothing" + value isa Symbol && return repr(value) + value isa AbstractString && return repr(String(value)) + value isa Bool && return value ? "true" : "false" + value isa Integer && return repr(value) + value isa AbstractFloat && return repr(value) + if value isa Tuple || value isa AbstractVector + items = String[_julia_literal(item) for item in value] + return "(" * join(items, ',') * (length(items) == 1 ? "," : "") * ")" + elseif value isa Pair + return _julia_literal(value.first) * " => " * _julia_literal(value.second) + end + return repr(value) +end +function _http_retrieval_base(resource; include_path::Bool = true) + uri = resource.retrieval.uri + scheme = lowercase(uri.scheme) + scheme in ("http", "https") || return nothing + isempty(uri.host) && return nothing + host = occursin(':', uri.host) && !startswith(uri.host, '[') ? + "[" * uri.host * "]" : uri.host + authority = scheme * "://" * host * + (isempty(uri.port) ? "" : ":" * uri.port) + include_path || return authority + return authority * (isempty(uri.path) ? "/" : uri.path) +end -""" - storefile(api_call::Function; - folder::AbstractString = pwd(), - rename_file::String="", - )::Tuple{Any,ApiResponse,String} +function _default_server(api::NormalizedAPI) + if isempty(api.servers) + return something( + _http_retrieval_base(api.source.resource; include_path = false), + "http://127.0.0.1:8080", + ) + end + server = first(api.servers) + url = server.url + for variable in server.variables + url = replace(url, "{" * variable.name * "}" => variable.default) + end + if startswith(url, "http://") || startswith(url, "https://") + return String(rstrip(url, '/')) + end + resource = Resources.resource(api.registry, server.provenance.node.resource) + retrieval = _http_retrieval_base(resource) + if retrieval !== nothing + resolved = Resources.URIs.resolvereference( + Resources.URIs.URI(retrieval), + Resources.URIs.URI(url), + ) + return String(rstrip(string(resolved), '/')) + end + return String( + rstrip( + "http://127.0.0.1:8080" * (startswith(url, '/') ? url : "/" * url), + '/', + ), + ) +end - Helper method that stores the result of an API call that returns file - contents (as binary or text string) into a file. +function _server_descriptor( + servers, + api::NormalizedAPI, + fallback::Resources.NodeId, +) + Base.@nospecialize servers + if isempty(servers) + resource = Resources.resource(api.registry, fallback.resource) + return "((name = nothing, url = \"/\", base = " * + _julia_literal(something(_http_retrieval_base(resource), "")) * + ", variables = ()),)" + end + entries = String[] + for server in servers + resource = Resources.resource(api.registry, server.provenance.node.resource) + variables = String[ + "(name = " * repr(variable.name) * + ", default = " * repr(variable.default) * + ", values = " * _julia_literal(variable.values) * ")" for + variable in server.variables + ] + encoded_variables = "(" * join(variables, ',') * + (length(variables) == 1 ? "," : "") * ")" + push!( + entries, + "(name = " * _julia_literal(server.name) * + ", url = " * repr(server.url) * + ", base = " * _julia_literal( + startswith(lowercase(server.url), "http://") || + startswith(lowercase(server.url), "https://") ? "" : + something(_http_retrieval_base(resource), ""), + ) * + ", variables = " * encoded_variables * ")", + ) + end + return "(" * join(entries, ',') * (length(entries) == 1 ? "," : "") * ")" +end - Convenient to use it in a do block. Returns the path where file is stored additionally. +function _security_type(scheme::NormalizedSecurityScheme) + if scheme.type === :http + lowered = lowercase(something(scheme.scheme, "")) + lowered == "basic" && return :http_basic + lowered == "bearer" && return :http_bearer + return :http + elseif scheme.type in (:openidconnect, :open_id_connect) + return :openidconnect + elseif scheme.type in (:mutualtls, :mutual_tls) + return :mutualtls + end + return scheme.type +end - E.g.: - ``` - _result, _http_response, file = OpenAPI.Clients.storefile() do - # Invoke the OpenaPI method that returns file contents. - # This is the method that returns a tuple of (result, http_response). - # The result is the file contents as binary or text string. - fetch_file(api, "reports", "category1") +function _emit_security(io::IO, plan::ClientPlan) + println(io, "const _SECURITY_SCHEMES = Dict{String,NamedTuple}(") + for scheme in sort(collect(plan.api.security_schemes); by = item -> item.name) + print(io, " ", repr(scheme.name), " => (") + print(io, "type = ", repr(_security_type(scheme)), ", ") + print(io, "location = ", repr(something(scheme.location, :none)), ", ") + print(io, "name = ", repr(something(scheme.parameter_name, "")), ", ") + println(io, "scheme = ", repr(something(scheme.scheme, "")), "),") end - ``` + println(io, ")\n") +end - Parameters: +function _model_indices(plan::ClientPlan) + return Dict(model.name => index for (index, model) in enumerate(plan.models)) +end - - `api_call`: The OpenAPI function call that returns file contents (as binary or text string). See example in method description. - - `folder`: Location to store file, defaults to `pwd()`. - - `filename`: Use this filename, overrides any filename that may be there in the `Content-Disposition` header. +function _model_references(type::String, names) + output = String[] + for matched in eachmatch(r"[A-Za-z_][A-Za-z0-9_]*", type) + name = matched.match + name in names || continue + name in output || push!(output, name) + end + return output +end - Returns: (result, http_response, file_path) -""" -function storefile(api_call::Function; - folder::AbstractString = pwd(), - filename::Union{String,Nothing} = nothing, - )::Tuple{Any,ApiResponse,String} +function _cyclic_aliases(plan::ClientPlan) + aliases = Set(model.name for model in plan.models if model.kind === :alias) + edges = Dict( + model.name => _model_references(something(model.alias, ""), aliases) for + model in plan.models if model.kind === :alias + ) + function reaches(start, current, seen) + current in seen && return false + push!(seen, current) + for target in get(edges, current, String[]) + target == start && return true + reaches(start, target, seen) && return true + end + return false + end + return Set(name for name in aliases if reaches(name, name, Set{String}())) +end - result, http_response = api_call() +function _model_types(model::ModelPlan, wrapped_aliases) + if model.kind === :object + types = String[field.type for field in model.fields] + model.additional_type === nothing || push!(types, model.additional_type) + return types + elseif model.kind in (:oneof, :anyof) + return String[something(model.alias, "Any")] + elseif model.kind === :alias && model.name in wrapped_aliases + return String[something(model.alias, "Any")] + end + return String[] +end - if isnothing(filename) - filename = extract_filename(http_response) +function _forward_abstracts(plan::ClientPlan, wrapped_aliases) + indices = _model_indices(plan) + concrete = Set(model.name for model in plan.models if model.kind !== :alias) + union!(concrete, wrapped_aliases) + targets = copy(wrapped_aliases) + for (index, model) in enumerate(plan.models) + for type in _model_types(model, wrapped_aliases), target in _model_references(type, concrete) + get(indices, target, 0) > index && push!(targets, target) + end end + return targets +end - mkpath(folder) - filepath = joinpath(folder, filename) +function _rewrite_forward(type::String, index::Int, indices, targets) + output = type + for target in _model_references(type, targets) + get(indices, target, 0) > index || continue + output = replace(output, Regex("\\b" * target * "\\b") => "Abstract" * target) + end + return output +end - open(filepath, "w") do io - write(io, result) +function _emit_model( + io::IO, + model::ModelPlan, + index, + indices, + abstract_targets, + wrapped_aliases, +) + schema = _schema_descriptor(model.provenance.node) + direction = repr(model.direction) + if model.kind === :alias && model.name in wrapped_aliases + type = _rewrite_forward(model.alias, index, indices, abstract_targets) + supertype = model.name in abstract_targets ? " <: Abstract" * model.name : "" + println(io, "struct ", model.name, supertype) + println(io, " value::", type) + println(io, "end") + model.name in abstract_targets && println( + io, + "_decode(::Type{Abstract", + model.name, + "}, value) = _decode(", + model.name, + ", value)", + ) + println(io, "function _decode(::Type{", model.name, "}, value)") + println(io, " _validate_schema(", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") + println(io, " return ", model.name, "(_decode(", type, ", value))") + println(io, "end") + println(io, "function _encode(value::", model.name, ")") + println(io, " output = _encode(value.value)") + println(io, " return _validate_schema(", schema, ", output, ", repr("encoding " * model.name), "; direction = ", direction, ")") + println(io, "end\n") + return + elseif model.kind === :alias + println(io, "const ", model.name, " = ", model.alias, "\n") + return + elseif model.kind === :enum + supertype = model.name in abstract_targets ? " <: Abstract" * model.name : "" + println(io, "struct ", model.name, supertype) + println(io, " value::", model.alias) + println(io, " function ", model.name, "(value::", model.alias, ")") + println(io, " value in ", _julia_literal(model.values), " || throw(ArgumentError(\"invalid ", model.name, " value \$(repr(value))\"))") + println(io, " return new(value)") + println(io, " end") + println(io, "end") + model.name in abstract_targets && println( + io, + "_decode(::Type{Abstract", + model.name, + "}, value) = _decode(", + model.name, + ", value)", + ) + println(io, "function _decode(::Type{", model.name, "}, value)") + println(io, " _validate_schema(", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") + println(io, " return ", model.name, "(_decode(", model.alias, ", value))") + println(io, "end") + println(io, "function _encode(value::", model.name, ")") + println(io, " output = _encode(value.value)") + println(io, " return _validate_schema(", schema, ", output, ", repr("encoding " * model.name), "; direction = ", direction, ")") + println(io, "end") + println(io, "Base.string(value::", model.name, ") = string(value.value)\n") + return + elseif model.kind in (:oneof, :anyof) + type = _rewrite_forward(model.alias, index, indices, abstract_targets) + supertype = model.name in abstract_targets ? " <: Abstract" * model.name : "" + println(io, "struct ", model.name, supertype) + println(io, " value::", type) + println(io, "end") + model.name in abstract_targets && println( + io, + "_decode(::Type{Abstract", + model.name, + "}, value) = _decode(", + model.name, + ", value)", + ) + if model.discriminator !== nothing && + (!isempty(model.discriminator_mapping) || model.discriminator_default !== nothing) + println(io, "function _decode(::Type{", model.name, "}, value)") + println(io, " _validate_schema(", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") + println(io, " object = _object(value, ", repr(model.name), ")") + println(io, " tag = get(object, ", repr(model.discriminator), ", ABSENT)") + println(io, " tag isa Absent || tag isa AbstractString || throw(DecodeError(\"discriminator value must be a string for ", model.name, "\"))") + println(io, " selected = get(Dict(") + for (tag, target) in model.discriminator_mapping + node, type = target + println( + io, + " ", + repr(tag), + " => (", + type, + ", ", + _schema_descriptor(node), + "),", + ) + end + println(io, " ), tag isa Absent ? \"\" : String(tag), nothing)") + if model.discriminator_default !== nothing + node, type = model.discriminator_default + println( + io, + " selected === nothing && (selected = (", + type, + ", ", + _schema_descriptor(node), + "))", + ) + end + println(io, " selected === nothing && throw(DecodeError(\"unknown discriminator value \$(repr(tag)) for ", model.name, "\"))") + println(io, " _schema_valid(selected[2], value; direction = ", direction, ") || throw(DecodeError(\"discriminator-selected schema did not validate for ", model.name, "\"))") + println(io, " return ", model.name, "(_decode(selected[1], value))") + println(io, "end") + else + println(io, "function _decode(::Type{", model.name, "}, value)") + println(io, " _validate_schema(", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") + "Nothing" in model.values && println( + io, + " value === nothing && return ", + model.name, + "(nothing)", + ) + println(io, " matches = Any[]") + for (node, type) in model.variants + descriptor = _schema_descriptor(node) + println(io, " if _schema_valid(", descriptor, ", value; direction = ", direction, ")") + println(io, " push!(matches, _decode(", type, ", value))") + println(io, " end") + end + if model.kind === :oneof + println(io, " length(matches) == 1 || throw(DecodeError(\"oneOf value did not select exactly one variant of ", model.name, "\"))") + else + println(io, " isempty(matches) && throw(DecodeError(\"anyOf value did not select a variant of ", model.name, "\"))") + end + println(io, " return ", model.name, "(first(matches))") + println(io, "end") + end + println(io, "function _encode(value::", model.name, ")") + println(io, " output = _encode(value.value)") + println(io, " return _validate_schema(", schema, ", output, ", repr("encoding " * model.name), "; direction = ", direction, ")") + println(io, "end\n") + return end - return result, http_response, filepath + supertype = model.name in abstract_targets ? " <: Abstract" * model.name : "" + println(io, "Base.@kwdef struct ", model.name, supertype) + for field in model.fields + type = _rewrite_forward(field.type, index, indices, abstract_targets) + print(io, " ", field.name, "::", type) + field.default === nothing || print(io, " = ", field.default) + println(io) + end + if model.additional_type !== nothing + additional_type = _rewrite_forward( + model.additional_type, + index, + indices, + abstract_targets, + ) + println( + io, + " additional_properties::Dict{String,", + additional_type, + "} = Dict{String,", + additional_type, + "}()", + ) + end + println(io, "end") + model.name in abstract_targets && println( + io, + "_decode(::Type{Abstract", + model.name, + "}, value) = _decode(", + model.name, + ", value)", + ) + println(io, "function _decode(::Type{", model.name, "}, raw)") + println(io, " _validate_schema(", schema, ", raw, ", repr("decoding " * model.name), "; direction = ", direction, ")") + println(io, " value = _object(raw, ", repr(model.name), ")") + for field in model.fields + type = _rewrite_forward(field.type, index, indices, abstract_targets) + if field.required + println( + io, + " ", + field.name, + " = _decode(", + type, + ", _required(value, ", + repr(field.wire_name), + ", ", + repr(model.name), + "))", + ) + else + println( + io, + " ", + field.name, + " = haskey(value, ", + repr(field.wire_name), + ") ? _decode(", + type, + ", value[", + repr(field.wire_name), + "]) : ABSENT", + ) + end + end + known = Tuple(field.wire_name for field in model.fields) + if model.additional_type !== nothing + additional_type = _rewrite_forward(model.additional_type, index, indices, abstract_targets) + println(io, " additional_properties = Dict{String,", additional_type, "}()") + println(io, " for (key, item) in value") + println(io, " String(key) in ", _julia_literal(known), " && continue") + println(io, " additional_properties[String(key)] = _decode(", additional_type, ", item)") + println(io, " end") + else + println(io, " unknown = setdiff(String.(collect(keys(value))), collect(", _julia_literal(known), "))") + println(io, " isempty(unknown) || throw(DecodeError(\"unknown fields while decoding ", model.name, ": \" * join(unknown, \", \")))") + end + print(io, " return ", model.name, "(") + assignments = String[string(field.name, " = ", field.name) for field in model.fields] + model.additional_type === nothing || push!(assignments, "additional_properties = additional_properties") + println(io, "; ", join(assignments, ", "), ")") + println(io, "end") + println(io, "function _encode(value::", model.name, ")") + println(io, " output = JSON.Object{String,Any}()") + for field in model.fields + println(io, " value.", field.name, " isa Absent || (output[", repr(field.wire_name), "] = _encode(value.", field.name, "))") + end + if model.additional_type !== nothing + println(io, " for (key, item) in value.additional_properties") + println(io, " haskey(output, key) && throw(ArgumentError(\"additional property conflicts with declared field: \" * key))") + println(io, " output[key] = _encode(item)") + println(io, " end") + end + println(io, " return _validate_schema(", schema, ", output, ", repr("encoding " * model.name), "; direction = ", direction, ")") + println(io, "end\n") + println(io, "function _form_fields(value::", model.name, ")") + println(io, " output = Pair{String,Any}[]") + for field in model.fields + println( + io, + " value.", + field.name, + " isa Absent || push!(output, ", + repr(field.wire_name), + " => value.", + field.name, + ")", + ) + end + if model.additional_type !== nothing + println(io, " append!(output, collect(value.additional_properties))") + end + println(io, " return output") + println(io, "end\n") end -const content_disposition_re = r"filename\*?=['\"]?(?:UTF-\d['\"]*)?([^;\r\n\"']*)['\"]?;?" +function _schema_node(handle::Union{Nothing,SchemaHandle}) + handle === nothing && return nothing + compiled = handle.compiled + return compiled === nothing ? handle.node : compiled.root +end -""" - extract_filename(resp)::String +function _schema_descriptor(node::Union{Nothing,Resources.NodeId}) + node === nothing && return "nothing" + return "(resource = " * repr(string(node.resource)) * + ", pointer = " * repr(string(node.pointer)) * ")" +end -Extracts the filename from the `Content-Disposition` header of the HTTP response. -If not found, then creates a filename from the `Content-Type` header. -""" -extract_filename(resp::ApiResponse) = extract_filename(resp.raw) -function extract_filename(resp::HTTPLibResponse)::String - # attempt to extract filename from content-disposition header - content_disposition_str = get_response_header(resp, "content-disposition", "") - m = match(content_disposition_re, content_disposition_str) - if !isnothing(m) && !isempty(m.captures) && !isnothing(m.captures[1]) - return m.captures[1] - end - - # attempt to create a filename from content-type header - content_type_str = get_response_header(resp, "content-type", "") - return string("response", extension_from_mime(MIME(content_type_str))) -end - -function deep_object_serialize(dict::Dict, parent_key::String = "") - parts = Pair[] - for (key, value) in dict - new_key = parent_key == "" ? key : "$parent_key[$key]" - if isa(value, Dict) - append!(parts, collect(deep_object_serialize(value, new_key))) - elseif isa(value, Vector) - for (i, v) in enumerate(value) - push!(parts, "$new_key[$(i-1)]"=>"$v") +function _dialect_literal(value::SchemaEngine.Dialect) + return "SchemaEngine.Dialect(" * join( + ( + repr(value.name), + repr(value.uri), + repr(value.id_keyword), + repr(value.ref_siblings), + repr(value.modern_items), + repr(value.unevaluated), + repr(value.dynamic_refs), + repr(value.recursive_refs), + repr(value.applicator), + repr(value.validation), + ), + ", ", + ) * ")" +end + +function _append_encoding_schema_handles!(handles, encodings) + for encoding in encodings + for header in encoding.headers + header.schema === nothing || push!(handles, header.schema) + for header_media in header.content + header_media.schema === nothing || push!(handles, header_media.schema) + end + end + _append_encoding_schema_handles!(handles, encoding.encoding) + end + return handles +end + +function _source_schema_node(registry, node::Resources.NodeId) + resource = Resources.resource(registry, node.resource) + source = resource.source + source_resource = Resources.resource(registry, source.resource) + pointer = Resources.JSONPointer( + (source.pointer.tokens..., node.pointer.tokens...), + ) + return Resources.NodeId(source_resource.id, pointer) +end + +function _directional_required_rules(plan::ClientPlan, template) + registry = template.registry + directional_cache = Dict{Tuple{Resources.NodeId,String},Bool}() + rules = Dict{ + Resources.NodeId, + Tuple{Set{String},Set{String}}, + }() + nodes = values(getfield(template, :evaluation_nodes)) + for node in nodes + schema = node.value + schema isa AbstractDict || continue + required = get(schema, "required", nothing) + required isa AbstractVector || continue + view = SchemaView( + schema, + node.id, + plan.api.source.version, + template, + ) + members, _ = _object_members(view) + properties = Dict(member[1] => member[2] for member in members) + input = Set{String}() + output = Set{String}() + for raw_name in required + name = String(raw_name) + property = get(properties, name, nothing) + property === nothing && continue + _has_directional_property( + property, + "readOnly", + directional_cache, + ) && push!(input, name) + _has_directional_property( + property, + "writeOnly", + directional_cache, + ) && push!(output, name) + end + (isempty(input) && isempty(output)) && continue + target = _source_schema_node(registry, node.id) + existing = get!(rules, target) do + (Set{String}(), Set{String}()) + end + union!(existing[1], input) + union!(existing[2], output) + end + output = collect(rules) + sort!( + output; + by = entry -> ( + string(entry.first.resource), + string(entry.first.pointer), + ), + ) + return output +end + +function _client_schema_handles(plan::ClientPlan) + handles = SchemaHandle[last(pair) for pair in plan.api.schemas] + for operation in plan.api.operations + operation.direction === :request || continue + for parameter in operation.parameters + parameter.schema === nothing || push!(handles, parameter.schema) + for media in parameter.content + media.schema === nothing || push!(handles, media.schema) + end + end + if operation.request_body !== nothing + for media in operation.request_body.content + media.schema === nothing || push!(handles, media.schema) + _append_encoding_schema_handles!(handles, media.encoding) + end + end + for response in operation.responses + for media in response.content + media.schema === nothing || push!(handles, media.schema) + end + for header in response.headers + header.schema === nothing || push!(handles, header.schema) + for media in header.content + media.schema === nothing || push!(handles, media.schema) + end end - else - push!(parts, "$new_key"=>"$value") end end - return Dict(parts) + unique!(handle -> handle.node, handles) + sort!( + handles; + by = handle -> (string(handle.node.resource), string(handle.node.pointer)), + ) + return handles +end + +function _emit_schema_data(io::IO, plan::ClientPlan) + handles = _client_schema_handles(plan) + graph = isempty(handles) ? nothing : first(handles).workspace.compiled + if graph === nothing + println(io, "const _SCHEMA_RESOURCE_DATA = Any[]") + println(io, "const _SCHEMA_ROOT_DATA = Any[]") + println(io, "const _SCHEMA_DIALECT_DATA = Any[]") + println(io, "const _SCHEMA_DIRECTIONAL_REQUIRED = Any[]\n") + return + end + template = getfield(graph, :template) + registry = template.registry + resources = Resources.Resource[ + resource for resource in values(getfield(registry, :resources)) + if isempty(resource.source.pointer) + ] + sort!(resources; by = resource -> string(resource.id)) + # Do not emit large metadata collections as tuples. A tuple literal encodes + # every entry in its Julia type. Real descriptions can contain tens of + # thousands of schema roots and directional rules, which makes top-level + # type hashing and subtyping take minutes. An Any vector keeps stable values + # without creating a giant tuple type. + println(io, "const _SCHEMA_RESOURCE_DATA = Any[") + for resource in resources + println( + io, + " (id = ", + repr(string(resource.id)), + ", retrieval = ", + repr(string(resource.retrieval)), + ", media_type = ", + _julia_literal(resource.media_type), + ", json = ", + repr(JSON.json(resource.contents)), + "),", + ) + end + println(io, "]") + + roots = Resources.NodeId[_schema_node(handle) for handle in handles] + evaluation_nodes = getfield(template, :evaluation_nodes) + for resource in resources + root = Resources.NodeId(resource.id, Resources.JSONPointer()) + haskey(evaluation_nodes, root) && push!(roots, root) + end + unique!(roots) + sort!(roots; by = node -> (string(node.resource), string(node.pointer))) + println(io, "const _SCHEMA_ROOT_DATA = Any[") + for root in roots + node = get(evaluation_nodes, root, nothing) + node === nothing && continue + println( + io, + " (resource = ", + repr(string(root.resource)), + ", pointer = ", + repr(string(root.pointer)), + ", dialect = ", + _dialect_literal(node.dialect), + "),", + ) + end + println(io, "]") + println(io, "const _SCHEMA_DIALECT_DATA = Any[") + aliases = collect(getfield(template, :dialect_aliases)) + sort!(aliases; by = first) + for (uri, dialect) in aliases + println( + io, + " (uri = ", + repr(uri), + ", name = ", + repr(dialect.name), + ", id_keyword = ", + repr(dialect.id_keyword), + ", ref_siblings = ", + repr(dialect.ref_siblings), + ", modern_items = ", + repr(dialect.modern_items), + ", unevaluated = ", + repr(dialect.unevaluated), + ", dynamic_refs = ", + repr(dialect.dynamic_refs), + ", recursive_refs = ", + repr(dialect.recursive_refs), + ", applicator = ", + repr(dialect.applicator), + ", validation = ", + repr(dialect.validation), + "),", + ) + end + println(io, "]") + println(io, "const _SCHEMA_DIRECTIONAL_REQUIRED = Any[") + for (node, removals) in _directional_required_rules(plan, template) + println( + io, + " (resource = ", + repr(string(node.resource)), + ", pointer = ", + repr(string(node.pointer)), + ", input = ", + _julia_literal(Tuple(sort!(collect(removals[1])))), + ", output = ", + _julia_literal(Tuple(sort!(collect(removals[2])))), + "),", + ) + end + println(io, "]\n") + return +end + +function _parameter_descriptor(parameter::ParameterPlan) + content = if isempty(parameter.parameter.content) + "()" + else + media = Pair{String,String}[ + item.content_type => "Any" for item in parameter.parameter.content + ] + _media_descriptor(media, parameter.parameter.content) + end + return "(arg = " * repr(Symbol(parameter.name)) * + ", name = " * repr(parameter.wire_name) * + ", type = " * parameter.type * + ", location = " * repr(parameter.location) * + ", style = " * repr(something(parameter.style, :none)) * + ", explode = " * (parameter.explode === true ? "true" : "false") * + ", allow_reserved = " * (parameter.allow_reserved ? "true" : "false") * + ", schema = " * + _schema_descriptor(_schema_node(_parameter_schema(parameter.parameter))) * + ", content = " * content * + ", required = " * (parameter.required ? "true" : "false") * ")" +end + +function _named_encoding_properties(view::Union{Nothing,SchemaView}) + view === nothing && return Dict{String,SchemaView}() + members, _ = _object_members(view) + return Dict(member[1] => member[2] for member in members) end -function request_supports_interrupt() - for m in methods(request) - if :interrupt in Base.kwarg_decl(m) - return true +function _named_encoding_value_view(view::SchemaView) + types = _without_null_type(_effective_types(view)) + if "array" in types || _keyword_owner(view, "items") !== nothing + owner = _keyword_owner(view, "items") + if owner !== nothing + raw = owner.value["items"] + (raw isa AbstractDict || raw isa Bool) && raw !== false && + return _child_view(owner, "items") end end - return false + return view end -end # module Clients +function _encoding_content_base(content_type) + content_type === nothing && return "" + selected = strip(first(split(String(content_type), ','; limit = 2))) + return lowercase(strip(first(split(selected, ';'; limit = 2)))) +end + +function _encoding_descriptor( + encoding::NormalizedEncoding, + base_media_type::String, + value_view::Union{Nothing,SchemaView}, +) + header_descriptors = String[] + if startswith(base_media_type, "multipart/") + for header in encoding.headers + push!(header_descriptors, _header_descriptor(header, "Any")) + end + end + encoded_headers = "(" * join(header_descriptors, ',') * + (length(header_descriptors) == 1 ? "," : "") * ")" + + nested_descriptors = String[] + nested_properties = value_view === nothing ? Dict{String,SchemaView}() : + _named_encoding_properties( + _named_encoding_value_view(value_view), + ) + nested_base = _encoding_content_base(encoding.content_type) + for nested in encoding.encoding + nested_view = get(nested_properties, nested.name, nothing) + nested_view === nothing && continue + push!( + nested_descriptors, + _encoding_descriptor(nested, nested_base, nested_view), + ) + end + encoded_nested = "(" * join(nested_descriptors, ',') * + (length(nested_descriptors) == 1 ? "," : "") * ")" + + return "(" * + "name = " * repr(encoding.name) * + ", content_type = " * _julia_literal(encoding.content_type) * + ", headers = " * encoded_headers * + ", encoding = " * encoded_nested * + ", style = " * _julia_literal(encoding.style) * + ", explode = " * _julia_literal(encoding.explode) * + ", allow_reserved = " * _julia_literal(encoding.allow_reserved) * + ", rfc6570 = " * _julia_literal( + base_media_type in ( + "application/x-www-form-urlencoded", + "multipart/form-data", + ) && + ( + encoding.style !== nothing || + encoding.explode !== nothing || + haskey(encoding.raw, "allowReserved") + ), + ) * + ")" +end + +function _media_descriptor(media, normalized) + Base.@nospecialize media normalized + items = String[] + for (entry, source) in zip(media, normalized) + base_media_type = lowercase( + strip(first(split(String(entry.first), ';'; limit = 2))), + ) + properties = source.schema === nothing ? Dict{String,SchemaView}() : + _named_encoding_properties(SchemaView(source.schema)) + encodings = String[] + for encoding in source.encoding + value_view = get(properties, encoding.name, nothing) + value_view === nothing && continue + push!( + encodings, + _encoding_descriptor(encoding, base_media_type, value_view), + ) + end + encoded = "(" * join(encodings, ',') * + (length(encodings) == 1 ? "," : "") * ")" + push!( + items, + "(" * repr(entry.first) * ", " * entry.second * ", " * + _schema_descriptor(_schema_node(source.schema)) * ", " * encoded * + ")", + ) + end + return "(" * join(items, ',') * (length(items) == 1 ? "," : "") * ")" +end + +function _schema_shape(handle::Union{Nothing,SchemaHandle}) + handle === nothing && return :scalar + view = _resolved_view(SchemaView(handle)) + _is_object_schema(view) && return :object + types = _schema_types(view.value) + ( + "array" in types || + view.value isa AbstractDict && + (haskey(view.value, "items") || haskey(view.value, "prefixItems")) + ) && + return :array + return :scalar +end + +function _header_descriptor(header::NormalizedHeader, type::String) + schema = header.schema !== nothing ? header.schema : + isempty(header.content) ? nothing : first(header.content).schema + content = isempty(header.content) ? "()" : + _media_descriptor( + Pair{String,String}[media.content_type => type for media in header.content], + header.content, + ) + return "(name = " * repr(header.name) * + ", type = " * type * + ", required = " * repr(header.required) * + ", shape = " * repr(_schema_shape(schema)) * + ", explode = " * repr(header.explode) * + ", schema = " * _schema_descriptor(_schema_node(schema)) * + ", content = " * content * ")" +end + +function _security_descriptor(requirements) + Base.@nospecialize requirements + alternatives = String[] + for requirement in requirements + entries = String[ + "(" * repr(name) * ", " * _julia_literal(scopes) * ")" for + (name, scopes) in requirement.alternatives + ] + push!(alternatives, "(" * join(entries, ',') * (length(entries) == 1 ? "," : "") * ")") + end + return "(" * join(alternatives, ',') * (length(alternatives) == 1 ? "," : "") * ")" +end + +function _emit_operation_descriptor(io, operation::OperationPlan, api::NormalizedAPI) + const_name = "_OP_" * operation.name + println(io, "const ", const_name, " = (") + println(io, " id = ", repr(operation.operation.id), ",") + println(io, " method = ", repr(String(operation.operation.method)), ",") + println(io, " path = ", repr(operation.operation.path), ",") + parameters = String[] + for parameter in operation.parameters + push!(parameters, _parameter_descriptor(parameter)) + end + println(io, " parameters = (", join(parameters, ','), length(parameters) == 1 ? "," : "", "),") + if operation.request_body === nothing + println(io, " request = nothing,") + else + request = operation.request_body + println( + io, + " request = (required = ", + request.required ? "true" : "false", + ", media = ", + _media_descriptor(request.media_types, request.body.content), + "),", + ) + end + println(io, " responses = (") + for response in operation.responses + headers = String[] + for (header, type) in zip( + response.response.headers, + response.header_types, + ) + push!(headers, _header_descriptor(header, type.second)) + end + encoded_headers = "(" * join(headers, ',') * + (length(headers) == 1 ? "," : "") * ")" + println( + io, + " (selector = ", + repr(response.selector), + ", media = ", + _media_descriptor(response.media_types, response.response.content), + ", headers = ", + encoded_headers, + "),", + ) + end + println(io, " ),") + println(io, " security = ", _security_descriptor(operation.operation.security), ",") + println( + io, + " servers = ", + _server_descriptor( + operation.operation.servers, + api, + operation.operation.provenance.node, + ), + ",", + ) + println(io, ")\n") + return const_name +end + +function _ordered_path_parameters(operation::OperationPlan) + byname = Dict{String,ParameterPlan}() + for parameter in operation.parameters + parameter.location === :path || continue + byname[parameter.wire_name] = parameter + end + output = ParameterPlan[] + seen = Set{String}() + for match in eachmatch(r"\{([^{}]+)\}", operation.operation.path) + name = String(match.captures[1]) + haskey(byname, name) && !(name in seen) || continue + push!(seen, name) + push!(output, byname[name]) + end + return output +end + +function _emit_operation(io, operation::OperationPlan, const_name) + path_parameters = _ordered_path_parameters(operation) + path_names = Set{String}() + for parameter in path_parameters + push!(path_names, parameter.name) + end + required_keywords = ParameterPlan[] + optional_keywords = ParameterPlan[] + for parameter in operation.parameters + parameter.name in path_names && continue + push!( + parameter.required ? required_keywords : optional_keywords, + parameter, + ) + end + positional = String[] + for parameter in path_parameters + push!(positional, string(parameter.name, "::", parameter.type)) + end + if operation.request_body !== nothing && operation.request_body.required + push!(positional, "body::" * operation.request_body.type) + end + keywords = String[] + for parameter in required_keywords + push!(keywords, parameter.name * "::" * parameter.type) + end + for parameter in optional_keywords + push!(keywords, parameter.name * "::" * parameter.type * " = ABSENT") + end + if operation.request_body !== nothing && !operation.request_body.required + push!(keywords, "body::" * operation.request_body.type * " = ABSENT") + end + has_multipart_request = operation.request_body !== nothing && any( + media -> startswith( + lowercase(strip(first(split(media.content_type, ';'; limit = 2)))), + "multipart/", + ), + operation.request_body.body.content, + ) + has_multipart_request && push!(keywords, "multipart_headers = NamedTuple()") + append!( + keywords, + [ + "client::Client = DEFAULT_CLIENT", + "content_type::Union{Nothing,AbstractString} = nothing", + "accept::Union{Nothing,AbstractString} = nothing", + "with_http_info::Bool = false", + "request_headers = Pair{String,String}[]", + "request_options::NamedTuple = NamedTuple()", + ], + ) + summary = something(operation.operation.summary, operation.operation.description, "") + println(io, "\"\"\"") + println(io, " ", operation.name, "(...)") + isempty(summary) || println(io, "\n", summary) + println(io, "\n`", operation.operation.method, " ", operation.operation.path, "`") + println(io, "\"\"\"") + println( + io, + "function ", + operation.name, + "(", + join(positional, ", "), + "; ", + join(keywords, ", "), + ")", + ) + println(io, " values = Dict{Symbol,Any}()") + for parameter in operation.parameters + println(io, " values[", repr(Symbol(parameter.name)), "] = ", parameter.name) + end + body_expression = operation.request_body === nothing ? "ABSENT" : "body" + print( + io, + " return _request(client, ", + const_name, + ", values; body = ", + body_expression, + ", content_type, accept, with_http_info, request_headers, request_options", + ) + has_multipart_request && print(io, ", multipart_headers") + println(io, ")") + println(io, "end\n") +end + +function _generate(plan::ClientPlan) + io = IOBuffer() + println( + io, + "# Generated by OpenAPI.jl from ", + repr(plan.api.title), + " version ", + plan.api.api_version, + ". Do not edit.", + ) + println(io, "module ", plan.module_name, "\n") + println(io, "using HTTP, JSON, OpenAPI, Base64, Dates, UUIDs") + println(io, "const SchemaEngine = OpenAPI.SchemaEngine\n") + _emit_security(io, plan) + _emit_schema_data(io, plan) + default_server = _default_server(plan.api) + print(io, GENERATED_RUNTIME, '\n') + println(io, "const _DEFAULT_SERVER = ", repr(default_server)) + println(io, "const SERVER = Ref{String}(_DEFAULT_SERVER)") + println(io, "const DEFAULT_CLIENT = Client()\n") + + indices = _model_indices(plan) + wrapped_aliases = _cyclic_aliases(plan) + abstract_targets = _forward_abstracts(plan, wrapped_aliases) + for target in sort(collect(abstract_targets)) + println(io, "abstract type Abstract", target, " end") + end + isempty(abstract_targets) || println(io) + for (index, model) in enumerate(plan.models) + _emit_model( + io, + model, + index, + indices, + abstract_targets, + wrapped_aliases, + ) + end + for operation in plan.operations + const_name = _emit_operation_descriptor(io, operation, plan.api) + _emit_operation(io, operation, const_name) + end + println(io, "end # module ", plan.module_name) + return String(take!(io)) +end + +""" + OpenAPI.client(source; name="ApiClient", path=nothing, strict=true, options...) -> String + +Generate a deterministic Julia client module after full OpenAPI loading, +reference binding, semantic normalization, and type planning. The generated +module supports OpenAPI 3.0, 3.1, and 3.2 request/response models, parameter +styles, content negotiation, and security requirements. +""" +function client( + source; + name::AbstractString = "ApiClient", + path::Union{Nothing,AbstractString} = nothing, + strict::Bool = true, + kwargs..., +) + client_plan = source isa ClientPlan ? source : plan(source; name, strict, kwargs...) + output = _generate(client_plan) + if path !== nothing + open(path, "w") do io + write(io, output) + end + end + return output +end diff --git a/src/client/chunk_readers.jl b/src/client/chunk_readers.jl deleted file mode 100644 index d038e31..0000000 --- a/src/client/chunk_readers.jl +++ /dev/null @@ -1,149 +0,0 @@ -struct LineChunkReader <: AbstractChunkReader - buffered_input::Base.BufferStream -end - -function Base.iterate(iter::LineChunkReader, _state=nothing) - if eof(iter.buffered_input) - return nothing - else - out = IOBuffer() - while !eof(iter.buffered_input) - byte = read(iter.buffered_input, UInt8) - (byte == codepoint('\n')) && break - write(out, byte) - end - return (take!(out), iter) - end -end - -struct JSONChunkReader <: AbstractChunkReader - buffered_input::Base.BufferStream -end - -function _read_json_chunk(io::IO) - out = IOBuffer() - first_byte = peek(io, UInt8) - - if first_byte == UInt8('{') || first_byte == UInt8('[') - close_byte = first_byte == UInt8('{') ? UInt8('}') : UInt8(']') - depth = 0 - in_string = false - escaped = false - complete = false - - while !eof(io) - byte = read(io, UInt8) - write(out, byte) - - if escaped - escaped = false - continue - end - - if in_string - if byte == UInt8('\\') - escaped = true - elseif byte == UInt8('"') - in_string = false - end - else - if byte == UInt8('"') - in_string = true - elseif byte == first_byte - depth += 1 - elseif byte == close_byte - depth -= 1 - if depth == 0 - complete = true - break - end - end - end - end - # The stream ended before the structure was balanced: the bytes read are a - # truncated document (a mid-stream connection close / cancelled response). - # Discard them instead of handing a partial document to the JSON parser, - # which would throw "Unexpected end of input". Returning empty makes - # `iterate` treat this as a clean end of stream. - complete || return UInt8[] - elseif first_byte == UInt8('"') - escaped = false - complete = false - read(io, UInt8) # consume opening quote - write(out, UInt8('"')) - while !eof(io) - byte = read(io, UInt8) - write(out, byte) - if escaped - escaped = false - elseif byte == UInt8('\\') - escaped = true - elseif byte == UInt8('"') - complete = true - break - end - end - # Truncated string (stream closed before the closing quote): discard. - complete || return UInt8[] - else - # number / true / false / null: read until delimiter - while !eof(io) - byte = peek(io, UInt8) - if isspace(Char(byte)) || byte == UInt8(',') || byte == UInt8(']') || byte == UInt8('}') - break - end - write(out, read(io, UInt8)) - end - end - - take!(out) -end - -function Base.iterate(iter::JSONChunkReader, _state=nothing) - if eof(iter.buffered_input) - return nothing - else - # read all whitespaces - while !eof(iter.buffered_input) - byte = peek(iter.buffered_input, UInt8) - if isspace(Char(byte)) - read(iter.buffered_input, UInt8) - else - break - end - end - eof(iter.buffered_input) && return nothing - chunk_bytes = _read_json_chunk(iter.buffered_input) - isempty(chunk_bytes) && return nothing - valid_json = _json_parse(String(chunk_bytes)) - bytes = convert(Vector{UInt8}, codeunits(JSON.json(valid_json))) - return (bytes, iter) - end -end - -# Ref: https://www.rfc-editor.org/rfc/rfc7464.html -const RFC7464_RECORD_SEPARATOR = UInt8(0x1E) -struct RFC7464ChunkReader <: AbstractChunkReader - buffered_input::Base.BufferStream -end - -function Base.iterate(iter::RFC7464ChunkReader, _state=nothing) - if eof(iter.buffered_input) - return nothing - else - out = IOBuffer() - while !eof(iter.buffered_input) - byte = read(iter.buffered_input, UInt8) - if byte == RFC7464_RECORD_SEPARATOR - bytes = take!(out) - if isnothing(_state) || !isempty(bytes) - return (bytes, iter) - end - else - write(out, byte) - end - end - bytes = take!(out) - return (bytes, iter) - end -end diff --git a/src/client/clienttypes.jl b/src/client/clienttypes.jl deleted file mode 100644 index 34fdb04..0000000 --- a/src/client/clienttypes.jl +++ /dev/null @@ -1,224 +0,0 @@ -abstract type AbstractChunkReader end -abstract type AbstractHTTPLibError end -const HTTPLibResponse = Union{HTTP.Response, Downloads.Response} -const HTTPLibError = Union{Downloads.RequestError, AbstractHTTPLibError} - -# methods to get exception messages out of errors which could be surfaced either as request or response errors -get_message(::HTTPLibError) = "" -get_message(::HTTPLibResponse) = "" -get_response(::HTTPLibError) = nothing -get_status(::HTTPLibError) = 0 - -# collection formats (OpenAPI v2) -# TODO: OpenAPI v3 has style and explode options instead of collection formats, which are yet to be supported -# TODO: Examine whether multi is now supported -const COLL_MULTI = "multi" # (legacy) aliased to CSV, as multi is not supported by Requests.jl (https://github.com/JuliaWeb/Requests.jl/issues/140) -const COLL_PIPES = "pipes" -const COLL_SSV = "ssv" -const COLL_TSV = "tsv" -const COLL_CSV = "csv" -const COLL_DLM = Dict{String,String}([COLL_PIPES=>"|", COLL_SSV=>" ", COLL_TSV=>"\t", COLL_CSV=>",", COLL_MULTI=>","]) - -const DEFAULT_TIMEOUT_SECS = 5*60 -const DEFAULT_LONGPOLL_TIMEOUT_SECS = 15*60 - -const HTTPLib = ( - HTTP = :http, - Downloads = :downloads -) - -struct ApiException <: Exception - status::Int - reason::String - resp::Union{Nothing, HTTPLibResponse} - error::Union{Nothing, HTTPLibError} - - function ApiException(error::HTTPLibError; reason::String="") - isempty(reason) && (reason = get_message(error)) - resp = get_response(error) - status = get_status(error) - new(status, reason, resp, error) - end -end - -""" - ApiResponse - -Represents the HTTP API response from the server. This is returned as the second return value from all API calls. - -Properties available: -- `status`: the HTTP status code -- `message`: the HTTP status message -- `headers`: the HTTP headers -- `raw`: the raw response from the HTTP library used -""" -struct ApiResponse - raw::HTTPLibResponse -end - -get_response_property(raw::HTTPLibResponse, name::Symbol) = getproperty(raw, name) -function Base.getproperty(resp::ApiResponse, name::Symbol) - raw = getfield(resp, :raw) - if name in (:status, :message, :headers) - return get_response_property(raw, name) - else - return getfield(resp, name) - end -end - - -function get_api_return_type(return_types::Dict{Regex,Type}, ::Nothing, response_data::String) - # this is the async case, where we do not have the response code yet - # in such cases we look for the 200 response code - return get_api_return_type(return_types, 200, response_data) -end -function get_api_return_type(return_types::Dict{Regex,Type}, response_code::Integer, response_data::String) - default_response_code = 0 - for code in string.([response_code, default_response_code]) - for (re, rt) in return_types - if match(re, code) !== nothing - return rt - end - end - end - # if no specific return type was defined, we assume that: - # - if response code is 2xx, then we make the method call return nothing - # - otherwise we make it throw an ApiException - return (200 <= response_code <=206) ? Nothing : nothing # first(return_types)[2] -end - -function default_debug_hook(type, message) - @info("OpenAPI HTTP transport", type, message) -end - -""" - Client(root::String; - headers::Dict{String,String}=Dict{String,String}(), - get_return_type::Function=get_api_return_type, - long_polling_timeout::Int=DEFAULT_LONGPOLL_TIMEOUT_SECS, - timeout::Int=DEFAULT_TIMEOUT_SECS, - pre_request_hook::Function=noop_pre_request_hook, - escape_path_params::Union{Nothing,Bool}=nothing, - chunk_reader_type::Union{Nothing,Type{<:AbstractChunkReader}}=nothing, - verbose::Union{Bool,Function}=false, - httplib::Symbol=HTTPLib.Downloads, - ) - -Create a new OpenAPI client context. - -A client context holds common information to be used across APIs. It also holds a connection to the server and uses that across API calls. -The client context needs to be passed as the first parameter of all API calls. - -Parameters: -- `root`: The root URL of the server. This is the base URL that will be used for all API calls. - -Keyword parameters: -- `headers`: A dictionary of HTTP headers to be sent with all API calls. -- `get_return_type`: A function that is called to determine the return type of an API call. This function is called with the following parameters: - - `return_types`: A dictionary of regular expressions and their corresponding return types. The regular expressions are matched against the HTTP status code of the response. - - `response_code`: The HTTP status code of the response. - - `response_data`: The response data as a string. - The function should return the return type to be used for the API call. -- `long_polling_timeout`: The timeout in seconds for long polling requests. This is the time after which the request will be aborted if no data is received from the server. -- `timeout`: The timeout in seconds for all other requests. This is the time after which the request will be aborted if no data is received from the server. -- `pre_request_hook`: A function that is called before every API call. This function must provide two methods: - - `pre_request_hook(ctx::Ctx)`: This method is called before every API call. It is passed the context object that will be used for the API call. The function should return the context object to be used for the API call. - - `pre_request_hook(resource_path::AbstractString, body::Any, headers::Dict{String,String})`: This method is called before every API call. It is passed the resource path, request body and request headers that will be used for the API call. The function should return those after making any modifications to them. -- `escape_path_params`: Whether the path parameters should be escaped before being used in the URL. This is useful if the path parameters contain characters that are not allowed in URLs or contain path separators themselves. -- `chunk_reader_type`: The type of chunk reader to be used for streaming responses. This can be one of `LineChunkReader`, `JSONChunkReader` or `RFC7464ChunkReader`. If not specified, then the type is automatically determined based on the return type of the API call. -- `verbose`: Can be set either to a boolean or a function (function support depends on the HTTP library). - - If set to true, then the client will log all HTTP requests and responses. - - If set to a function (only supported with Downloads.jl backend), then that function will be called with the following parameters: - - `type`: The type of message. - - `message`: The message to be logged. - - Note: When using HTTP.jl backend (`httplib=OpenAPI.HTTPLib.HTTP`), the `verbose` parameter must be a boolean. -- `httplib`: The HTTP client library to use for making requests. Can be `OpenAPI.HTTPLib.Downloads` (default) for Downloads.jl or `OpenAPI.HTTPLib.HTTP` for HTTP.jl. - -""" -struct Client - root::String - headers::Dict{String,String} - get_return_type::Function # user provided hook to get return type from response data - clntoptions::Dict{Symbol,Any} - downloader::Union{Nothing,Downloader} - timeout::Ref{Int} - pre_request_hook::Function # user provided hook to modify the request before it is sent - escape_path_params::Union{Nothing,Bool} - chunk_reader_type::Union{Nothing,Type{<:AbstractChunkReader}} - long_polling_timeout::Int - request_interrupt_supported::Bool - httplib::Symbol # which http implementation to use - - function Client(root::String; - headers::Dict{String,String}=Dict{String,String}(), - get_return_type::Function=get_api_return_type, - long_polling_timeout::Int=DEFAULT_LONGPOLL_TIMEOUT_SECS, - timeout::Int=DEFAULT_TIMEOUT_SECS, - pre_request_hook::Function=noop_pre_request_hook, - escape_path_params::Union{Nothing,Bool}=nothing, - chunk_reader_type::Union{Nothing,Type{<:AbstractChunkReader}}=nothing, - verbose::Union{Bool,Function}=false, - httplib::Symbol=:http, - ) - # Validate library choice - if httplib ∉ values(HTTPLib) - throw(ArgumentError("Invalid httplib: $httplib")) - end - - clntoptions = Dict{Symbol,Any}(:throw=>false) - if isa(verbose, Bool) - clntoptions[:verbose] = verbose - elseif isa(verbose, Function) - if httplib === HTTPLib.HTTP - throw(ArgumentError("With HTTP.jl, `verbose` can only be a boolean")) - end - clntoptions[:debug] = verbose - end - - if httplib === HTTPLib.HTTP - downloader = nothing - interruptable = false - else - downloader = Downloads.Downloader() - downloader.easy_hook = (easy, opts) -> begin - Downloads.Curl.setopt(easy, LibCURL.CURLOPT_LOW_SPEED_TIME, long_polling_timeout) - # disable ALPN to support servers that enable both HTTP/2 and HTTP/1.1 on same port - Downloads.Curl.setopt(easy, LibCURL.CURLOPT_SSL_ENABLE_ALPN, 0) - end - - interruptable = request_supports_interrupt() - end - new(root, headers, get_return_type, clntoptions, downloader, Ref{Int}(timeout), pre_request_hook, escape_path_params, chunk_reader_type, long_polling_timeout, interruptable, httplib) - end -end - -struct Ctx - client::Client - method::String - return_types::Dict{Regex,Type} - resource::String - auth::Vector{String} - - path::Dict{String,String} - query::Dict{String,String} - header::Dict{String,String} - form::Dict{String,String} - file::Dict{String,String} - body::Any - timeout::Int - curl_mime_upload::Ref{Any} - pre_request_hook::Function - escape_path_params::Bool - chunk_reader_type::Union{Nothing,Type{<:AbstractChunkReader}} - - function Ctx(client::Client, method::String, return_types::Dict{Regex,Type}, resource::String, auth, body=nothing; - timeout::Int=client.timeout[], - pre_request_hook::Function=client.pre_request_hook, - escape_path_params::Bool=something(client.escape_path_params, true), - chunk_reader_type::Union{Nothing,Type{<:AbstractChunkReader}}=client.chunk_reader_type, - ) - resource = client.root * resource - headers = copy(client.headers) - new(client, method, return_types, resource, auth, Dict{String,String}(), Dict{String,String}(), headers, Dict{String,String}(), Dict{String,String}(), body, timeout, Ref{Any}(nothing), pre_request_hook, escape_path_params, chunk_reader_type) - end -end diff --git a/src/client/httplibs/httplibs.jl b/src/client/httplibs/httplibs.jl deleted file mode 100644 index 2fda8e2..0000000 --- a/src/client/httplibs/httplibs.jl +++ /dev/null @@ -1,60 +0,0 @@ -# ============================================================================= -# HTTP Backend Interface Contract -# ============================================================================= -# -# Each HTTP backend implementation must provide the following functions: -# -# 1. Request Preparation (via Val dispatch) -# prep_args(::Val{:backend_symbol}, ctx::Ctx) -> (body, kwargs) -# -# Prepares request body and HTTP library-specific options from the context. -# - Handles content-type detection and setting -# - Processes form data and file uploads -# - Converts body to appropriate format (JSON, form-encoded, etc.) -# - Returns tuple of (body, kwargs) for the HTTP library -# -# 2. Request Execution (via Val dispatch) -# do_request(::Val{:backend_symbol}, ctx::Ctx, resource_path::String, -# body, output, kwargs, stream::Bool; stream_to::Union{Channel,Nothing}) -# -> (response, output) -# -# Executes the HTTP request using the backend library. -# - Performs synchronous or streaming request based on `stream` flag -# - Handles task management for streaming responses -# - Returns tuple of (response, output) or (error, output) on failure -# -# 3. Response Header Access (via Type dispatch) -# get_response_header(resp::BackendResponse, name::AbstractString, -# defaultval::AbstractString) -> String -# -# Retrieves a header value from the backend-specific response object. -# Case-insensitive header name matching required. -# -# 4. Error Information Extraction (via Type dispatch) -# get_message(error::BackendError) -> String -# get_response(error::BackendError) -> Union{Nothing, BackendResponse} -# get_status(error::BackendError) -> Int -# -# Extracts error information from backend-specific error objects. -# - get_message: Human-readable error description -# - get_response: Associated response object (if available) -# - get_status: HTTP status code (0 if no response available) -# -# 5. Response Property Access (via Type dispatch, optional) -# get_response_property(raw::BackendResponse, name::Symbol) -> Any -# -# Provides access to backend-specific response properties. -# Only needed if backend response type doesn't directly support -# required properties (status, message, headers). -# -# ============================================================================= -# Available Backend Implementations -# ============================================================================= -# -# :downloads (OpenAPI.HTTPLib.Downloads) - Uses Downloads.jl from Julia stdlib -# :http (OpenAPI.HTTPLib.HTTP) - Uses HTTP.jl from JuliaWeb ecosystem -# -# ============================================================================= - -include("juliaweb_http.jl") -include("julialang_downloads.jl") \ No newline at end of file diff --git a/src/client/httplibs/julialang_downloads.jl b/src/client/httplibs/julialang_downloads.jl deleted file mode 100644 index f711b14..0000000 --- a/src/client/httplibs/julialang_downloads.jl +++ /dev/null @@ -1,242 +0,0 @@ -# ============================================================================= -# Downloads.jl Backend Implementation -# ============================================================================= -# This file implements the HTTP client backend using the Downloads.jl library. -# -# Dependencies: -# - Downloads (stdlib): Primary HTTP client library -# - LibCURL: For low-level cURL operations (file uploads, MIME handling) -# - URIs: For URI escaping and query parameter handling -# -# Public Interface (via Val dispatch): -# - prep_args(::Val{:downloads}, ctx::Ctx) -# - do_request(::Val{:downloads}, ctx, ...) -# -# Type-Specific Methods: -# - get_response_header(::Downloads.Response, ...) -# - get_message(::Downloads.RequestError) -# - get_response(::Downloads.RequestError) -# - get_status(::Downloads.RequestError) -# ============================================================================= - -function _downloads_get_content_type(headers::Dict{String,String}) - for (name, value) in headers - if lowercase(name) == "content-type" - return value - end - end - return nothing -end - -function prep_args(::Val{:downloads}, ctx::Ctx) - kwargs = copy(ctx.client.clntoptions) - kwargs[:downloader] = ctx.client.downloader # use the default downloader for most cases - - isempty(ctx.file) && (ctx.body === nothing) && isempty(ctx.form) && !("Content-Length" in keys(ctx.header)) && (ctx.header["Content-Length"] = "0") - headers = ctx.header - body = nothing - - content_type_set = _downloads_get_content_type(headers) - if !isnothing(content_type_set) - content_type_set = lowercase(content_type_set) - end - - if !isempty(ctx.form) - if !isnothing(content_type_set) && content_type_set !== "multipart/form-data" && content_type_set !== "application/x-www-form-urlencoded" - throw(OpenAPIException("Content type already set to $content_type_set. To send form data, it must be multipart/form-data or application/x-www-form-urlencoded.")) - end - if isnothing(content_type_set) - if !isempty(ctx.file) - headers["Content-Type"] = content_type_set = "multipart/form-data" - else - headers["Content-Type"] = content_type_set = "application/x-www-form-urlencoded" - end - end - if content_type_set == "application/x-www-form-urlencoded" - body = URIs.escapeuri(ctx.form) - else - # we shall process it along with file uploads where we send multipart/form-data - end - end - - if !isempty(ctx.file) || (content_type_set == "multipart/form-data") - if !isnothing(content_type_set) && content_type_set !== "multipart/form-data" - throw(OpenAPIException("Content type already set to $content_type_set. To send file, it must be multipart/form-data.")) - end - - if isnothing(content_type_set) - headers["Content-Type"] = content_type_set = "multipart/form-data" - end - - # use a separate downloader for file uploads - # until we have something like https://github.com/JuliaLang/Downloads.jl/pull/148 - downloader = Downloads.Downloader() - downloader.easy_hook = (easy, opts) -> begin - Downloads.Curl.setopt(easy, LibCURL.CURLOPT_LOW_SPEED_TIME, ctx.client.long_polling_timeout) - mime = ctx.curl_mime_upload[] - if mime === nothing - mime = LibCURL.curl_mime_init(easy.handle) - ctx.curl_mime_upload[] = mime - end - for (_k,_v) in ctx.file - part = LibCURL.curl_mime_addpart(mime) - LibCURL.curl_mime_name(part, _k) - LibCURL.curl_mime_filedata(part, _v) - # TODO: make provision to call curl_mime_type in future? - end - for (_k,_v) in ctx.form - # add multipart sections for form data as well - part = LibCURL.curl_mime_addpart(mime) - LibCURL.curl_mime_name(part, _k) - LibCURL.curl_mime_data(part, _v, length(_v)) - end - Downloads.Curl.setopt(easy, LibCURL.CURLOPT_MIMEPOST, mime) - end - kwargs[:downloader] = downloader - end - - if ctx.body !== nothing - (isempty(ctx.form) && isempty(ctx.file)) || throw(OpenAPIException("Can not send both form-encoded data and a request body")) - if is_json_mime(something(content_type_set, "application/json")) - body = to_json(ctx.body) - elseif ("application/x-www-form-urlencoded" == content_type_set) && isa(ctx.body, Dict) - body = URIs.escapeuri(ctx.body) - elseif isa(ctx.body, APIModel) && isnothing(content_type_set) - headers["Content-Type"] = content_type_set = "application/json" - body = to_json(ctx.body) - else - body = ctx.body - end - end - - kwargs[:timeout] = ctx.timeout - kwargs[:method] = uppercase(ctx.method) - kwargs[:headers] = headers - - return body, kwargs -end - -function get_response_header(resp::Downloads.Response, name::AbstractString, defaultval::AbstractString) - for (n,v) in resp.headers - (lowercase(n) == lowercase(name)) && (return v) - end - return defaultval -end - -function get_message(error::Downloads.RequestError) - reason = error.message - isempty(reason) && (reason = error.response.message) - return reason -end - -function get_response(error::Downloads.RequestError) - return error.response -end - -function get_status(error::Downloads.RequestError) - return error.response.status -end - -function do_request(::Val{:downloads}, ctx::Ctx, resource_path::String, body, output, kwargs, stream::Bool=false; stream_to::Union{Channel,Nothing}=nothing) - resp = nothing - try - input = nothing - if body !== nothing - input = PipeBuffer() - write(input, body) - end - - if stream - interrupt = nothing - if ctx.client.request_interrupt_supported - kwargs[:interrupt] = interrupt = Base.Event() - end - @sync begin - download_task = @async begin - try - resp = Downloads.request(resource_path; - input=input, - output=output, - kwargs... - ) - catch ex - # If request method does not support interrupt natively, InterrptException is used to - # signal the download task to stop. Otherwise, InterrptException is not handled and is rethrown. - # Any exception other than InterruptException is rethrown always. - if ctx.client.request_interrupt_supported || !isa(ex, InterruptException) - @error("exception invoking request", exception=(ex,catch_backtrace())) - rethrow() - end - finally - close(output) - end - end - @async begin - try - if isnothing(ctx.chunk_reader_type) - default_return_type = ctx.client.get_return_type(ctx.return_types, nothing, "") - readerT = default_return_type <: APIModel ? JSONChunkReader : LineChunkReader - else - readerT = ctx.chunk_reader_type - end - for chunk in readerT(output) - return_type = ctx.client.get_return_type(ctx.return_types, nothing, String(copy(chunk))) - data = response(return_type, resp, chunk) - put!(stream_to, data) - end - catch ex - if !isa(ex, InvalidStateException) && isopen(stream_to) - @error("exception reading chunk", exception=(ex,catch_backtrace())) - rethrow() - end - finally - close(stream_to) - end - end - @async begin - interrupted = false - while isopen(stream_to) - try - wait(stream_to) - yield() - catch ex - isa(ex, InvalidStateException) || rethrow(ex) - interrupted = true - if !istaskdone(download_task) - # If the download task is still running, interrupt it. - # If it supports interrupt natively, then use event to signal it. - # Otherwise, throw an InterruptException to stop the download task. - if ctx.client.request_interrupt_supported - notify(interrupt) - else - schedule(download_task, InterruptException(), error=true) - end - end - end - end - if !interrupted && !istaskdone(download_task) - if ctx.client.request_interrupt_supported - notify(interrupt) - else - schedule(download_task, InterruptException(), error=true) - end - end - end - end - else - resp = Downloads.request(resource_path; - input=input, - output=output, - kwargs... - ) - close(output) - end - finally - if ctx.curl_mime_upload[] !== nothing - LibCURL.curl_mime_free(ctx.curl_mime_upload[]) - ctx.curl_mime_upload[] = nothing - end - end - - return resp, output -end diff --git a/src/client/httplibs/juliaweb_http.jl b/src/client/httplibs/juliaweb_http.jl deleted file mode 100644 index cadf0a0..0000000 --- a/src/client/httplibs/juliaweb_http.jl +++ /dev/null @@ -1,419 +0,0 @@ -# ============================================================================= -# HTTP.jl Backend Implementation -# ============================================================================= -# This file implements the HTTP client backend using the HTTP.jl (JuliaWeb) library. -# -# Dependencies: -# - HTTP: Primary HTTP client library from JuliaWeb -# - URIs: For URI escaping and query parameter handling -# -# Public Interface (via Val dispatch): -# - prep_args(::Val{:http}, ctx::Ctx) -# - do_request(::Val{:http}, ctx, ...) -# -# Type-Specific Methods: -# - get_response_property(::HTTP.Response, ...) -# - get_response_header(::HTTP.Response, ...) -# - get_message(::HTTPRequestError) -# - get_response(::HTTPRequestError) -# - get_status(::HTTPRequestError) -# -# Custom Types: -# - HTTPRequestError <: AbstractHTTPLibError -# ============================================================================= - -# HTTP.jl 2.0 reworked much of the public API; we support both 1.x and 2.x and -# branch on the major version at load time. `pkgversion` exists from Julia 1.9; -# on older Julia only HTTP 1.x can be installed (HTTP 2.0 needs Julia >= 1.10). -const _HTTP_V2 = isdefined(Base, :pkgversion) && something(pkgversion(HTTP), v"1") >= v"2" - -# Status reason text: `HTTP.Messages.statustext` on 1.x, `Response.reason` on 2.x. -function _http_statustext(raw::HTTP.Response) - if isdefined(HTTP, :Messages) - return HTTP.Messages.statustext(raw.status) - elseif hasproperty(raw, :reason) && !isempty(raw.reason) - return raw.reason - else - return string(raw.status) - end -end - -# Case-insensitive header lookup over the request header collection (a Dict). -# Avoids `HTTP.Header`/`HTTP.header(::Vector, ...)`, both removed in 2.0. -function _header_value_ci(headers, key::AbstractString) - lk = lowercase(key) - for (k, v) in headers - lowercase(String(k)) == lk && return String(v) - end - return nothing -end - -# Form content type: `HTTP.content_type` returns a `Pair` on 1.x, a `String` on 2.x. -_http_form_content_type(body) = (ct = HTTP.content_type(body); ct isa Pair ? ct[2] : ct) - -# Timeout budget in ms: `TimeoutError.readtimeout` (s) on 1.x, `.timeout_ns` on 2.x. -# Keep this an integer — the reason string is later matched against `\d+ milliseconds`. -_http_timeout_ms(e::HTTP.TimeoutError) = - hasproperty(e, :readtimeout) ? e.readtimeout * 1000 : e.timeout_ns ÷ 1_000_000 - -# Underlying cause of a connect error: `.error` on 1.x, `.cause` on 2.x. -_http_connect_cause(e::HTTP.ConnectError) = hasproperty(e, :cause) ? e.cause : e.error - -# Message for a generic HTTP error. HTTP 2.x introduces a dedicated `HTTP.DNSError` -# (a subtype of HTTP.HTTPError) for name-resolution failures, where 1.x instead wraps a -# `Sockets.DNSError` inside a ConnectError. The two stringify differently -# (`"HTTP.DNSError(...)"` vs `"DNSError: ..."`); normalize the 2.x form so the surfaced -# reason starts with "DNSError" on both, keeping the message stable across versions. -_http_error_message(error::HTTP.HTTPError) = string(error) -@static if _HTTP_V2 - _http_error_message(error::HTTP.DNSError) = "DNSError: could not resolve host \"$(error.hostname)\"" -end - -# Inactivity-timeout keyword: 1.x calls it `readtimeout`; 2.0 renamed it to -# `read_idle_timeout` (`readtimeout` still works but emits a deprecation warning). -_http_read_timeout_kw(timeout) = _HTTP_V2 ? (; read_idle_timeout=timeout) : (; readtimeout=timeout) - -# Transport protocol selection on HTTP.jl 2.x. 2.x defaults to `prefer_http2=true` -# and transparently upgrades any ALPN-capable TLS server to HTTP/2. We default to -# HTTP/1.1 (`:h1`) instead, because OpenAPI's streaming abort model — interrupt the -# read task and close the stream when the consumer closes the channel — assumes one -# request per connection, as in HTTP/1.1. Observed behavior over a reused HTTP/2 -# connection: after a few aborted watch/streaming cycles the shared connection's read -# loop wedges (the k8s watch hang), most likely from per-stream state left behind by -# the aborts. Callers who want the 2.x default can set `:http_protocol => :auto` -# (or `:h2`) in the client options. HTTP/1.x has no `protocol` keyword, so pass none. -_http_protocol_kw(ctx) = - _HTTP_V2 ? (; protocol=get(ctx.client.clntoptions, :http_protocol, :h1)) : (;) - -function get_response_property(raw::HTTP.Response, name::Symbol) - if name === :message - return _http_statustext(raw) - else - return getproperty(raw, name) - end -end - -function get_response_header(resp::HTTP.Response, name::AbstractString, defaultval::AbstractString) - return HTTP.header(resp, name, defaultval) -end - -struct HTTPRequestError <: AbstractHTTPLibError - message::String - error::HTTP.HTTPError - response::Union{Nothing,HTTP.Response} - - function HTTPRequestError(error::HTTP.TimeoutError, bytesread::Int, response::Union{Nothing,HTTP.Response}) - message = "Operation timed out after $(_http_timeout_ms(error)) milliseconds with $(bytesread) bytes received" - new(message, error, response) - end - - function HTTPRequestError(error::HTTP.TimeoutError, response::Union{Nothing,HTTP.Response}) - message = "Operation timed out after $(_http_timeout_ms(error)) milliseconds" - new(message, error, response) - end - - function HTTPRequestError(error::HTTP.ConnectError) - cause = _http_connect_cause(error) - message = if isa(cause, CapturedException) - string(cause.ex) - else - string(cause) - end - new(message, error, nothing) - end - - function HTTPRequestError(error::HTTP.HTTPError) - message = _http_error_message(error) - new(message, error, nothing) - end -end - -_http_as_request_error(args...) = nothing -_http_as_request_error(ex::HTTP.HTTPError, args...) = return HTTPRequestError(ex) -_http_as_request_error(ex::HTTP.ConnectError, args...) = return HTTPRequestError(ex) -_http_as_request_error(ex::HTTP.TimeoutError, args...) = return HTTPRequestError(ex, args...) -_http_as_request_error(ex::TaskFailedException, args...) = _http_as_request_error(ex.task.exception, args...) - -function _http_as_request_error(ex::CompositeException, args...) - for ex in ex.exceptions - request_error = _http_as_request_error(ex, args...) - if !isnothing(request_error) - return request_error - end - end - return nothing -end - -get_response(error::HTTPRequestError) = error.response -function get_message(error::HTTPRequestError) - return error.message -end -function get_status(error::HTTPRequestError) - if isnothing(error.response) - return 0 - else - return error.response.status - end -end - -function prep_args(::Val{:http}, ctx::Ctx) - kwargs = copy(ctx.client.clntoptions) - - isempty(ctx.file) && (ctx.body === nothing) && isempty(ctx.form) && !("Content-Length" in keys(ctx.header)) && (ctx.header["Content-Length"] = "0") - headers = ctx.header - body = nothing - - content_type_set = _header_value_ci(headers, "Content-Type") - if !isnothing(content_type_set) - content_type_set = lowercase(content_type_set) - end - - if !isempty(ctx.form) - if !isnothing(content_type_set) && content_type_set !== "multipart/form-data" && content_type_set !== "application/x-www-form-urlencoded" - throw(OpenAPIException("Content type already set to $content_type_set. To send form data, it must be multipart/form-data or application/x-www-form-urlencoded.")) - end - if isnothing(content_type_set) - if !isempty(ctx.file) - headers["Content-Type"] = content_type_set = "multipart/form-data" - else - headers["Content-Type"] = content_type_set = "application/x-www-form-urlencoded" - end - end - if content_type_set == "application/x-www-form-urlencoded" - body = URIs.escapeuri(ctx.form) - else - # we shall process it along with file uploads where we send multipart/form-data - end - end - - openhandles = Any[] - try - if !isempty(ctx.file) || (content_type_set == "multipart/form-data") - if !isnothing(content_type_set) && content_type_set !== "multipart/form-data" - throw(OpenAPIException("Content type already set to $content_type_set. To send file, it must be multipart/form-data.")) - end - - body_dict = Dict{String,Any}() - - for (_k,_v) in ctx.file - if isfile(_v) - fhandle = open(_v) - push!(openhandles, fhandle) - body_dict[_k] = fhandle - else - body_dict[_k] = HTTP.Multipart(_k, IOBuffer(_v)) - end - end - - for (_k,_v) in ctx.form - body_dict[_k] = _v - end - body = HTTP.Form(body_dict) - headers["Content-Type"] = content_type_set = _http_form_content_type(body) - end - - if ctx.body !== nothing - (isempty(ctx.form) && isempty(ctx.file)) || throw(OpenAPIException("Can not send both form-encoded data and a request body")) - if is_json_mime(something(content_type_set, "application/json")) - body = to_json(ctx.body) - elseif ("application/x-www-form-urlencoded" == content_type_set) && isa(ctx.body, Dict) - body = URIs.escapeuri(ctx.body) - elseif isa(ctx.body, APIModel) && isnothing(content_type_set) - headers["Content-Type"] = content_type_set = "application/json" - body = to_json(ctx.body) - else - body = ctx.body - end - end - - kwargs[:timeout] = ctx.timeout - kwargs[:method] = uppercase(ctx.method) - kwargs[:headers] = headers - kwargs[:openhandles] = openhandles - catch - # if prep_args fails after opening handles, ensure they are closed - for fhandle in openhandles - close(fhandle) - end - rethrow() - end - - return body, kwargs -end - -function do_request(::Val{:http}, ctx::Ctx, resource_path::String, body, output, kwargs, stream::Bool=false; stream_to::Union{Channel,Nothing}=nothing) - method = kwargs[:method] - timeout_secs = kwargs[:timeout] - openhandles = kwargs[:openhandles] - headers_dict = kwargs[:headers] - headers = [k => v for (k, v) in headers_dict] - bytesread = Ref{Int}(0) - captured_response = Ref{Union{Nothing,HTTP.Response}}(nothing) - - if body === nothing - body = UInt8[] - end - - try - if stream - return _http_streaming_request(ctx, method, resource_path, headers, body, timeout_secs, bytesread, captured_response, output, stream_to) - else - return _http_request(ctx, method, resource_path, headers, body, timeout_secs, bytesread, captured_response, output) - end - catch ex - possible_request_error = _http_as_request_error(ex, bytesread[], captured_response[]) - if !isnothing(possible_request_error) - return possible_request_error, output - else - rethrow(ex) - end - finally - for fhandle in openhandles - close(fhandle) - end - end -end - -function _http_request(ctx, method, url, headers, body, timeout, bytesread, captured_response, output) - captured_response[] = http_response = HTTP.request(method, url, headers, body; - _http_read_timeout_kw(timeout)..., - _http_protocol_kw(ctx)..., - connect_timeout=timeout ÷ 2, - retry=false, - redirect=true, - status_exception=false, - verbose=get(ctx.client.clntoptions, :verbose, false)) - - bytesread[] += write(output, http_response.body) - close(output) - - return http_response, output -end - -function _http_streaming_request(ctx, method, url, headers, body, timeout, bytesread, captured_response, output, stream_to) - http_response = nothing - - # HTTP.jl 2.0's `HTTP.open` does not accept a `verbose` keyword; only pass it on 1.x. - open_kwargs = merge(_http_read_timeout_kw(timeout), - _http_protocol_kw(ctx), - (; connect_timeout=timeout ÷ 2, - retry=false, - redirect=true, - status_exception=false)) - if !_HTTP_V2 - open_kwargs = merge(open_kwargs, (; verbose=get(ctx.client.clntoptions, :verbose, false))) - end - - # Capture the streaming connection so the abort-on-close watcher below can - # unblock the read task when the consumer closes `stream_to`. (Mirrors the - # `:downloads` backend, which interrupts its download task on channel close.) - io_ref = Ref{Any}(nothing) - - @sync begin - read_task = @async begin - try - HTTP.open(method, url, headers; open_kwargs...) do io - io_ref[] = io - write(io, body) - captured_response[] = http_response = startread(io) - try - # `readavailable`, not `readbytes!`: on both 1.x and 2.x streams - # `readbytes!(io, buf)` blocks until the whole buffer is filled - # (or the body ends), so a response chunk smaller than the buffer - # (e.g. a single Kubernetes watch event) is not forwarded until - # enough later data accumulates — an indefinite stall on quiet - # streams. `eof` blocks until data is available; `readavailable` - # then returns whatever has arrived without further blocking. - while !eof(io) - bytes = readavailable(io) - isempty(bytes) && continue - bytesread[] += write(output, bytes) - end - finally - close(output) - end - end - catch ex - close(output) - # When the consumer closes `stream_to`, the watcher task below aborts - # this read (close(io) + a scheduled InterruptException); that surfaces - # here as an IO error or InterruptException. Swallow it so the streaming - # request returns normally instead of propagating a spurious error. A - # read timeout or genuine network error arrives while `stream_to` is - # still open (and is not our InterruptException), so it still throws. - if isopen(stream_to) && !isa(ex, InterruptException) - rethrow(ex) - end - end - end - - @async begin - try - if isnothing(ctx.chunk_reader_type) - default_return_type = ctx.client.get_return_type(ctx.return_types, nothing, "") - readerT = default_return_type <: APIModel ? JSONChunkReader : LineChunkReader - else - readerT = ctx.chunk_reader_type - end - for chunk in readerT(output) - return_type = ctx.client.get_return_type(ctx.return_types, nothing, String(copy(chunk))) - data = response(return_type, nothing, chunk) # resp not available yet in streaming - put!(stream_to, data) - end - catch ex - if !isa(ex, InvalidStateException) && isopen(stream_to) - @error("exception reading chunk", exception=(ex,catch_backtrace())) - rethrow() - end - finally - close(stream_to) - end - end - - @async begin - # Abort-on-close watcher: when the consumer closes `stream_to` (e.g. a - # k8s watch is stopped via `close(stream)`), abort the read task above so - # it unblocks immediately instead of hanging on the socket until the - # read-idle timeout. Mirrors the `:downloads` backend, which interrupts - # its download task on channel close. - try - # Block until the consumer closes the channel. Do NOT use - # `wait(stream_to)`: it returns as soon as data is AVAILABLE, so - # while an event sits in the channel not yet consumed, a - # wait+yield loop degenerates into a hot spin that burns a full - # core in scheduler/syscall overhead. Poll `isopen` instead; - # 250ms of extra abort latency is irrelevant here. - while isopen(stream_to) - sleep(0.25) - end - catch ex - isa(ex, InvalidStateException) || rethrow(ex) - end - # Best-effort close of the connection. On HTTP/2 this alone does NOT wake a - # body read parked on the flow-control timer, so we also forcibly interrupt - # the read task (as `:downloads` does for non-interruptible downloads). - io = io_ref[] - # `io` may be `nothing` if the consumer closed before the connection was - # established; in practice a consumer only stops after receiving an event - # (or a timer fires seconds later), so the connection is already up. This - # is the same theoretical hole the `:downloads` watcher has. - if io !== nothing - try - close(io) - catch - # already closed / natural EOF — nothing to abort - end - end - # `stream_to` also closes on natural EOF (the chunk reader closes it after - # the read task finishes); the `istaskdone` guard skips the interrupt in - # that case. For JuliaRun's watch consumers an interrupt of an already- - # finishing read is harmless anyway — it maps to `is_request_interrupted`, - # the same signal a read-idle timeout produces, which they already retry on. - if !istaskdone(read_task) - schedule(read_task, InterruptException(); error=true) - end - end - end - - return http_response, output -end diff --git a/src/commontypes.jl b/src/commontypes.jl deleted file mode 100644 index abaef28..0000000 --- a/src/commontypes.jl +++ /dev/null @@ -1,22 +0,0 @@ -abstract type APIModel end -abstract type APIClientImpl end -abstract type UnionAPIModel <: APIModel end -abstract type OneOfAPIModel <: UnionAPIModel end -abstract type AnyOfAPIModel <: UnionAPIModel end -struct OpenAPIException <: Exception - reason::String -end -Base.@kwdef struct ValidationException <: Exception - reason::String - value=nothing - parameter=nothing - rule=nothing - args=nothing - operation_or_model=nothing -end -ValidationException(reason) = ValidationException(;reason) -struct InvocationException <: Exception - reason::String -end - -property_type(::Type{T}, name::Symbol) where {T<:APIModel} = error("invalid type $T") diff --git a/src/datetime.jl b/src/datetime.jl deleted file mode 100644 index 028217c..0000000 --- a/src/datetime.jl +++ /dev/null @@ -1,74 +0,0 @@ -const DATETIME_FORMATS = [ - Dates.DateFormat("yyyy-mm-dd"), - Dates.DateFormat("yyyy-mm-ddz"), - Dates.DateFormat("yyyy-mm-dd HH:MM:SS"), - Dates.DateFormat("yyyy-mm-ddTHH:MM:SS"), - Dates.DateFormat("yyyy-mm-dd HH:MM:SSz"), - Dates.DateFormat("yyyy-mm-ddTHH:MM:SSz"), - Dates.DateFormat("yyyy-mm-dd HH:MM:SS.sss"), - Dates.DateFormat("yyyy-mm-ddTHH:MM:SS.sss"), - Dates.DateFormat("yyyy-mm-dd HH:MM:SS.sssz"), - Dates.DateFormat("yyyy-mm-ddTHH:MM:SS.sssz"), - Dates.DateFormat("yyyy-mm-dd HH:MM:SS.ss"), - Dates.DateFormat("yyyy-mm-ddTHH:MM:SS.ss"), - Dates.DateFormat("yyyy-mm-dd HH:MM:SS.ssz"), - Dates.DateFormat("yyyy-mm-ddTHH:MM:SS.ssz"), - Dates.DateFormat("yyyy-mm-dd HH:MM:SS.s"), - Dates.DateFormat("yyyy-mm-ddTHH:MM:SS.s"), - Dates.DateFormat("yyyy-mm-dd HH:MM:SS.sz"), - Dates.DateFormat("yyyy-mm-ddTHH:MM:SS.sz"), -] - -const rxdatetime = - r"([0-9]{4}-[0-9]{2}-[0-9]{2}[T\s][0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,3})?)[0-9]*([+\-Z][:\.0-9]*)?" -function reduce_to_ms_precision(datetimestr::String) - matches = match(rxdatetime, datetimestr) - isnothing(matches) && return datetimestr - - c1 = matches.captures[1] - isnothing(c1) && return datetimestr - - c2 = matches.captures[2] - return isnothing(c2) ? String(c1) : c1 * c2 -end - -str2zoneddatetime(bytes::Vector{UInt8}) = str2zoneddatetime(String(bytes)) -function str2zoneddatetime(str::String) - str = reduce_to_ms_precision(str) - for fmt in DATETIME_FORMATS - try - return ZonedDateTime(str, fmt) - catch - # try next format - end - end - return ZonedDateTime(str2datetime(str), localzone()) -end -str2zoneddatetime(datetime::DateTime) = ZonedDateTime(datetime, localzone()) - -str2datetime(bytes::Vector{UInt8}) = str2datetime(String(bytes)) -function str2datetime(str::String) - str = reduce_to_ms_precision(str) - for fmt in DATETIME_FORMATS - try - return DateTime(str, fmt) - catch - # try next format - end - end - throw(OpenAPIException("Unsupported DateTime format: $str")) -end -str2datetime(datetime::DateTime) = datetime - -str2date(bytes::Vector{UInt8}) = str2date(String(bytes)) -function str2date(str::String) - for fmt in DATETIME_FORMATS - try - return Date(str, fmt) - catch - # try next format - end - end - throw(OpenAPIException("Unsupported Date format: $str")) -end -str2date(date::Date) = date diff --git a/src/diagnostics.jl b/src/diagnostics.jl new file mode 100644 index 0000000..5d91ddf --- /dev/null +++ b/src/diagnostics.jl @@ -0,0 +1,122 @@ +const Resources = SchemaEngine.Resources + +"""A one-based source position when a parser can report one.""" +struct SourcePosition + line::Int + column::Int + byte::Int +end + +"""A resource and JSON Pointer location inside an OpenAPI description.""" +struct SourceLocation + resource::Resources.ResourceId + pointer::Resources.JSONPointer + position::Union{Nothing,SourcePosition} +end + +function SourceLocation( + resource::Resources.ResourceId, + pointer::Resources.JSONPointer = Resources.JSONPointer(); + position::Union{Nothing,SourcePosition} = nothing, +) + return SourceLocation(resource, pointer, position) +end + +function Base.show(io::IO, location::SourceLocation) + pointer = string(location.pointer) + print(io, string(location.resource)) + isempty(pointer) || print(io, '#', pointer) + if location.position !== nothing + position = location.position + print(io, ':', position.line, ':', position.column) + end + return +end + +"""One stable, machine-readable OpenAPI diagnostic.""" +struct Diagnostic + severity::Symbol + code::Symbol + message::String + location::SourceLocation + context::Tuple{Vararg{Pair{String,String}}} + + function Diagnostic( + severity::Symbol, + code::Symbol, + message::AbstractString, + location::SourceLocation; + context = Pair{String,String}[], + ) + severity in (:error, :warning, :info) || + throw(ArgumentError("diagnostic severity must be :error, :warning, or :info")) + normalized = + Pair{String,String}[String(key) => String(value) for (key, value) in context] + return new(severity, code, String(message), location, Tuple(normalized)) + end +end + +function Base.show(io::IO, diagnostic::Diagnostic) + print( + io, + uppercase(String(diagnostic.severity)), + " [", + diagnostic.code, + "] ", + diagnostic.location, + ": ", + diagnostic.message, + ) + return +end + +"""An error that contains all diagnostics collected for one operation.""" +struct OpenAPIError <: Exception + summary::String + diagnostics::Vector{Diagnostic} +end + +function Base.showerror(io::IO, error::OpenAPIError) + print(io, error.summary) + for diagnostic in error.diagnostics + print(io, '\n', " ") + show(io, diagnostic) + end + return +end + +mutable struct DiagnosticBag + diagnostics::Vector{Diagnostic} + max_diagnostics::Int +end + +function DiagnosticBag(max_diagnostics::Integer = 1_000) + max_diagnostics > 0 || throw(ArgumentError("max_diagnostics must be positive")) + return DiagnosticBag(Diagnostic[], Int(max_diagnostics)) +end + +function _emit!( + bag::DiagnosticBag, + severity::Symbol, + code::Symbol, + message::AbstractString, + location::SourceLocation; + context = Pair{String,String}[], +) + length(bag.diagnostics) < bag.max_diagnostics || + throw(OpenAPIError("OpenAPI diagnostic limit reached", copy(bag.diagnostics))) + push!(bag.diagnostics, Diagnostic(severity, code, message, location; context)) + return last(bag.diagnostics) +end + +_error!(bag, code, message, location; kwargs...) = + _emit!(bag, :error, code, message, location; kwargs...) +_warning!(bag, code, message, location; kwargs...) = + _emit!(bag, :warning, code, message, location; kwargs...) + +haserrors(diagnostics) = any(diagnostic -> diagnostic.severity === :error, diagnostics) + +function _throw_on_errors(summary::AbstractString, diagnostics) + haserrors(diagnostics) || return + throw(OpenAPIError(String(summary), collect(diagnostics))) +end diff --git a/src/document.jl b/src/document.jl new file mode 100644 index 0000000..5420fc5 --- /dev/null +++ b/src/document.jl @@ -0,0 +1,260 @@ +const METHODS = (:GET, :POST, :PUT, :DELETE, :PATCH, :HEAD, :OPTIONS, :TRACE, :QUERY) + +""" + OpenAPI.Param(name, location, type; required=true) + +One path or query parameter of an [`Operation`](@ref): `location` is `:path` or +`:query`, `type` is the Julia type the value coerces to (drives the emitted +schema). +""" +struct Param + name::String + location::Symbol + type::Type + required::Bool + + function Param( + name::AbstractString, + location::Symbol, + type::Type = String; + required::Bool = true, + ) + location in (:path, :query) || throw( + ArgumentError( + "Param `$name`: location must be :path or :query, got :$location", + ), + ) + location == :path && + !required && + throw(ArgumentError("Param `$name`: path parameters are always required")) + return new(String(name), location, type, required) + end +end + +""" + OpenAPI.Operation(; id, method, path, kw...) + +A framework-neutral description of one endpoint, the input to +[`OpenAPI.document`](@ref). Anything that can describe its endpoints as +`Operation`s can use the same document-generation path. + +Keywords: +- `id::String` — the `operationId` (also the generated client's function name) +- `method::Symbol` — `:GET`, `:POST`, … (`$(METHODS)`) +- `path::String` — `/segment/{param}` template; placeholders must match the + `:path` params exactly +- `summary=""` — human description +- `params=Param[]` — path/query parameters +- `bodytype=nothing` — Julia type of the request body, or `nothing` for none +- `responsetype=nothing` — Julia type of the success response; `nothing` means + unknown (empty schema), the *type* `Nothing` means 204 No Content, and a + `Union{Nothing, T}` emits both 204 and a 200 with `T`'s schema +- `contenttype="application/json"` — media type for body/response content +- `secured=false` — whether the operation requires authentication (emitted as a + bearer security requirement) +""" +struct Operation + id::String + method::Symbol + path::String + summary::String + params::Vector{Param} + bodytype::Union{Nothing,Type} + responsetype::Any + contenttype::String + secured::Bool +end + +function Operation(; + id::AbstractString, + method::Symbol, + path::AbstractString, + summary::AbstractString = "", + params::Vector{Param} = Param[], + bodytype::Union{Nothing,Type} = nothing, + responsetype = nothing, + contenttype::AbstractString = "application/json", + secured::Bool = false, +) + method in METHODS || throw( + ArgumentError( + "operation `$id`: unsupported method `$method`; expected one of $(join(METHODS, ", "))", + ), + ) + startswith(path, "/") || + throw(ArgumentError("operation `$id`: path must start with '/': `$path`")) + placeholders = pathplaceholders(path) + pathnames = [p.name for p in params if p.location == :path] + for ph in placeholders + ph in pathnames || throw( + ArgumentError( + "operation `$id`: path parameter `{$ph}` in `$path` has no matching Param", + ), + ) + end + for p in pathnames + p in placeholders || throw( + ArgumentError( + "operation `$id`: Param `$p` is a path parameter but `$path` has no `{$p}` segment", + ), + ) + end + responsetype === nothing || + responsetype isa Type || + throw(ArgumentError("operation `$id`: responsetype must be a Type or nothing")) + return Operation( + String(id), + method, + String(path), + String(summary), + params, + bodytype, + responsetype, + String(contenttype), + secured, + ) +end + +function pathplaceholders(path::AbstractString) + names = String[] + for seg in split(path, '/'; keepempty = false) + m = match(r"^\{(\w+)\}$", seg) + if m !== nothing + name = String(m.captures[1]) + name in names && + throw(ArgumentError("duplicate path parameter `{$name}` in `$path`")) + push!(names, name) + elseif occursin('{', seg) || occursin('}', seg) + throw( + ArgumentError( + "malformed segment `$seg` in `$path`: a path parameter must be a full segment like `{name}`", + ), + ) + end + end + return names +end + +# A small framework-neutral default error envelope. +errorschema() = obj( + "type" => "object", + "properties" => obj( + "error" => obj( + "type" => "object", + "properties" => obj( + "message" => obj("type" => "string"), + "code" => obj("type" => "integer"), + ), + ), + ), +) + +""" + OpenAPI.document(operations; title="API", version="0.1.0", description="", servers=String[]) + -> JSON.Object + +Build a valid OpenAPI $(OPENAPI_VERSION) document from a vector of +[`Operation`](@ref)s. Named struct types encountered in parameter, body, and +response types are collected under `components/schemas` and referenced by +`\$ref`. Serialize with `JSON.json(doc)` (or `JSON.json(doc; pretty=2)`). + +Framework packages can add router-specific methods without becoming an OpenAPI +dependency. +""" +function document( + ops::Vector{Operation}; + title::AbstractString = "API", + version::AbstractString = "0.1.0", + description::AbstractString = "", + servers::Vector{String} = String[], +) + reg = SchemaRegistry() + paths = JSON.Object{String,Any}() + ids = Set{String}() + anysecured = false + for op in ops + id = op.id + i = 2 + while id in ids + id = string(op.id, "_", i) + i += 1 + end + push!(ids, id) + haskey(paths, op.path) || (paths[op.path] = JSON.Object{String,Any}()) + item = paths[op.path] + methodkey = lowercase(string(op.method)) + haskey(item, methodkey) && + throw(ArgumentError("duplicate operation for $(op.method) $(op.path)")) + o = obj("operationId" => id) + isempty(op.summary) || (o["summary"] = op.summary) + if !isempty(op.params) + parameters = Any[] + for p in op.params + parameter = obj( + "name" => p.name, + "in" => string(p.location), + "required" => p.required, + "schema" => schemaof(reg, p.type), + ) + p.location === :query && p.type <: AbstractVector && + (parameter["explode"] = false) + push!(parameters, parameter) + end + o["parameters"] = parameters + end + if op.bodytype !== nothing + o["requestBody"] = obj( + "required" => true, + "content" => + obj(op.contenttype => obj("schema" => schemaof(reg, op.bodytype))), + ) + end + responses = JSON.Object{String,Any}() + rt = op.responsetype + if rt === nothing + responses["200"] = obj("description" => "success") + else + if rt === Nothing + responses["204"] = obj("description" => "no content") + elseif Nothing <: rt + responses["204"] = obj("description" => "no content") + inner = Union{filter(t -> t !== Nothing, uniontypes(rt))...} + responses["200"] = obj( + "description" => "success", + "content" => + obj(op.contenttype => obj("schema" => schemaof(reg, inner))), + ) + else + responses["200"] = obj( + "description" => "success", + "content" => + obj(op.contenttype => obj("schema" => schemaof(reg, rt))), + ) + end + end + responses["default"] = obj( + "description" => "unexpected error", + "content" => obj("application/json" => obj("schema" => errorschema())), + ) + o["responses"] = responses + if op.secured + o["security"] = Any[obj("bearerAuth" => String[])] + anysecured = true + end + item[methodkey] = o + end + doc = obj("openapi" => OPENAPI_VERSION) + info = obj("title" => String(title), "version" => String(version)) + isempty(description) || (info["description"] = String(description)) + doc["info"] = info + isempty(servers) || (doc["servers"] = Any[obj("url" => s) for s in servers]) + doc["paths"] = paths + components = JSON.Object{String,Any}() + isempty(reg.schemas) || (components["schemas"] = reg.schemas) + anysecured && ( + components["securitySchemes"] = + obj("bearerAuth" => obj("type" => "http", "scheme" => "bearer")) + ) + isempty(components) || (doc["components"] = components) + return doc +end diff --git a/src/json.jl b/src/json.jl deleted file mode 100644 index 5ed7bd6..0000000 --- a/src/json.jl +++ /dev/null @@ -1,176 +0,0 @@ -# JSONWrapper for OpenAPI models handles -# - null fields -# - field names that are Julia keywords -struct JSONWrapper{T<:APIModel} <: AbstractDict{Symbol, Any} - wrapped::T - flds::Tuple -end - -JSONWrapper(o::T) where {T<:APIModel} = JSONWrapper(o, filter(n->hasproperty(o,n) && (getproperty(o,n) !== nothing), propertynames(o))) - -getindex(w::JSONWrapper, s::Symbol) = getproperty(w.wrapped, s) -keys(w::JSONWrapper) = w.flds -length(w::JSONWrapper) = length(w.flds) - -function iterate(w::JSONWrapper, state...) - result = iterate(w.flds, state...) - if result === nothing - return result - else - name,nextstate = result - val = getproperty(w.wrapped, name) - return (name=>val, nextstate) - end -end - -lower(o::T) where {T<:APIModel} = JSONWrapper(o) -function lower(o::T) where {T<:UnionAPIModel} - if typeof(o.value) <: UnionAPIModel - return lower(o.value) - elseif typeof(o.value) <: APIModel - return JSONWrapper(o.value) - elseif typeof(o.value) <: Union{String,Real} - return o.value - else - return to_json(o.value) - end -end - -struct StyleCtx - location::Symbol - name::String - is_explode::Bool -end - -is_deep_explode(sctx::StyleCtx) = sctx.name == "deepObject" && sctx.is_explode - -function deep_object_to_array(src::AbstractDict) - keys_are_int = all(key -> occursin(r"^\d+$", key), keys(src)) - if keys_are_int - sorted_keys = sort(collect(keys(src)), by=x->parse(Int, x)) - final = [] - for key in sorted_keys - push!(final, src[key]) - end - return final - else - src - end -end - -to_json(o) = JSON.json(o) - -from_json(::Type{Union{Nothing,T}}, json::AbstractDict{String,Any}; stylectx=nothing) where {T} = from_json(T, json; stylectx) -from_json(::Type{T}, json::AbstractDict{String,Any}; stylectx=nothing) where {T} = from_json(T(), json; stylectx) -from_json(::Type{T}, json::AbstractDict{String,Any}; stylectx=nothing) where {T <: Dict} = convert(T, Dict{String,Any}(json)) -from_json(::Type{T}, j::AbstractDict{String,Any}; stylectx=nothing) where {T <: String} = to_json(j) -from_json(::Type{Any}, j::AbstractDict{String,Any}; stylectx=nothing) = j -from_json(::Type{Vector{T}}, j::Vector{Any}; stylectx=nothing) where {T} = j - -function from_json(::Type{Vector{T}}, json::AbstractDict{String, Any}; stylectx=nothing) where {T} - if !isnothing(stylectx) && is_deep_explode(stylectx) - cvt = deep_object_to_array(json) - if isa(cvt, Vector) - return from_json(Vector{T}, cvt; stylectx) - else - return from_json(T, json; stylectx) - end - else - return from_json(T, json; stylectx) - end -end - -function from_json(o::T, json::AbstractDict{String,Any};stylectx=nothing) where {T <: UnionAPIModel} - return from_json(o, :value, json;stylectx) -end - -from_json(::Type{T}, val::Union{String,Real};stylectx=nothing) where {T <: UnionAPIModel} = T(val) -function from_json(o::T, val::Union{String,Real};stylectx=nothing) where {T <: UnionAPIModel} - o.value = val - return o -end - -function from_json(o::T, json::AbstractDict{String,Any};stylectx=nothing) where {T <: APIModel} - jsonkeys = [Symbol(k) for k in keys(json)] - for name in intersect(propertynames(o), jsonkeys) - from_json(o, name, json[String(name)];stylectx) - end - return o -end - -function from_json(o::T, name::Symbol, json::AbstractDict{String,Any};stylectx=nothing) where {T <: APIModel} - ftype = (T <: UnionAPIModel) ? property_type(T, name, Dict{String,Any}(json)) : property_type(T, name) - fval = from_json(ftype, json; stylectx) - setfield!(o, name, convert(ftype, fval)) - return o -end - -function from_json(o::T, name::Symbol, v; stylectx=nothing) where {T <: APIModel} - ftype = (T <: UnionAPIModel) ? property_type(T, name, Dict{String,Any}()) : property_type(T, name) - atype = isa(ftype, Union) ? ((ftype.a === Nothing) ? ftype.b : ftype.a) : ftype - if ftype === Any - setfield!(o, name, v) - elseif ZonedDateTime <: ftype - setfield!(o, name, str2zoneddatetime(v)) - elseif DateTime <: ftype - setfield!(o, name, str2datetime(v)) - elseif Date <: ftype - setfield!(o, name, str2date(v)) - elseif String <: ftype && isa(v, Real) - # string numbers can have format specifiers that allow numbers, ensure they are converted to strings - setfield!(o, name, string(v)) - elseif atype <: Real && isa(v, AbstractString) - setfield!(o, name, parse(atype, v)) - else - setfield!(o, name, convert(ftype, v)) - end - return o -end - -function from_json(o::T, name::Symbol, v::Vector; stylectx=nothing) where {T <: APIModel} - # in Julia we can not support JSON null unless the element type is explicitly set to support it - ftype = property_type(T, name) - - if ftype === Any - setfield!(o, name, v) - return o - end - - vtype = isa(ftype, Union) ? ((ftype.a === Nothing) ? ftype.b : ftype.a) : (ftype <: Vector) ? ftype : Union{} - veltype = eltype(vtype) - (Nothing <: veltype) || filter!(x->x!==nothing, v) - - if veltype === Any - setfield!(o, name, convert(ftype, v)) - elseif ZonedDateTime <: veltype - setfield!(o, name, map(str2zoneddatetime, v)) - elseif DateTime <: veltype - setfield!(o, name, map(str2datetime, v)) - elseif Date <: veltype - setfield!(o, name, map(str2date, v)) - else - if (vtype <: Vector) && (veltype <: OpenAPI.UnionAPIModel) - vec = veltype[] - for vecelem in v - push!(vec, from_json(veltype(), :value, vecelem;stylectx)) - end - setfield!(o, name, vec) - elseif (vtype <: Vector) && (veltype <: OpenAPI.APIModel) - setfield!(o, name, map(x->convert(veltype,x), v)) - elseif (vtype <: Vector) && (veltype <: String) - # ensure that elements are converted to String - # convert is to do the translation to Union{Nothing,String} when necessary - setfield!(o, name, convert(ftype, map(string, v))) - elseif ftype <: OpenAPI.UnionAPIModel - setfield!(o, name, ftype(v)) - else - setfield!(o, name, convert(ftype, v)) - end - end - return o -end - -function from_json(o::T, name::Symbol, ::Nothing;stylectx=nothing) where {T <: APIModel} - setfield!(o, name, nothing) - return o -end diff --git a/src/loading.jl b/src/loading.jl new file mode 100644 index 0000000..ea07c7f --- /dev/null +++ b/src/loading.jl @@ -0,0 +1,470 @@ +struct DocumentVersion + raw::String + major::Int + minor::Int + patch::Int + prerelease::Union{Nothing,String} +end + +function DocumentVersion(value::AbstractString) + matched = match(r"^(3)\.(0|1|2)\.([0-9]+)(?:-([0-9A-Za-z.-]+))?$", value) + matched === nothing && throw( + ArgumentError( + "unsupported OpenAPI version $(repr(value)); expected 3.0.x, 3.1.x, or 3.2.x", + ), + ) + return DocumentVersion( + String(value), + Base.parse(Int, matched.captures[1]), + Base.parse(Int, matched.captures[2]), + Base.parse(Int, matched.captures[3]), + matched.captures[4] === nothing ? nothing : String(matched.captures[4]), + ) +end + +oas_family(version::DocumentVersion) = Symbol("oas3", version.minor) + +"""An immutable, parsed OpenAPI source resource.""" +struct SourceDocument + resource::Resources.Resource + version::DocumentVersion + format::Symbol + locations::Dict{Resources.JSONPointer,SourcePosition} +end + +function Base.getproperty(document::SourceDocument, name::Symbol) + name === :locations && return copy(getfield(document, :locations)) + return getfield(document, name) +end + +Base.getindex(document::SourceDocument, key) = document.resource.contents[key] +Base.haskey(document::SourceDocument, key) = haskey(document.resource.contents, key) +Base.keys(document::SourceDocument) = keys(document.resource.contents) + +function location( + document::SourceDocument, + pointer::Resources.JSONPointer = Resources.JSONPointer(), +) + locations = getfield(document, :locations) + current = pointer + position = nothing + while true + position = get(locations, current, nothing) + position === nothing || break + isempty(current) && break + current = Resources.JSONPointer(current.tokens[1:(end - 1)]) + end + return SourceLocation(document.resource.id, pointer; position) +end + +const SPEC_SCHEMA_LOCK = ReentrantLock() +const SPEC_SCHEMA_CACHE = Dict{Int,Any}() + +function _schema_path(minor::Int) + return normpath(@__DIR__, "..", "schemas", "oas-3.$minor.json") +end + +function _spec_schema(minor::Int) + return lock(SPEC_SCHEMA_LOCK) do + return get!(SPEC_SCHEMA_CACHE, minor) do + return SchemaEngine.CompiledSchema(JSON.parsefile(_schema_path(minor))) + end + end +end + +function _file_id(path::AbstractString) + absolute = abspath(expanduser(path)) + uri = Resources.URIs.URI(; scheme = "file", path = absolute) + return Resources.ResourceId(uri) +end + +function _inline_id(base_uri) + base_uri === nothing && return Resources.ResourceId("urn:openapi:inline") + return base_uri isa Resources.ResourceId ? base_uri : Resources.ResourceId(base_uri) +end + +function _format_from_hint(hint::AbstractString) + lowered = lowercase(hint) + if endswith(lowered, ".json") || + occursin("application/json", lowered) || + occursin("+json", lowered) + return :json + elseif endswith(lowered, ".yaml") || + endswith(lowered, ".yml") || + occursin("yaml", lowered) + return :yaml + end + return :auto +end + +function _sniff_format(bytes::AbstractVector{UInt8}, hint::Symbol) + hint in (:json, :yaml) && return hint + text = lstrip(String(copy(bytes))) + (startswith(text, "{") || startswith(text, "[")) && return :json + return :yaml +end + +function _source_bytes( + source::AbstractString, + bag::DiagnosticBag; + base_uri = nothing, + format::Symbol = :auto, + max_bytes::Integer = 16 * 1024 * 1024, +) + max_bytes > 0 || throw(ArgumentError("max_bytes must be positive")) + format in (:auto, :json, :yaml) || + throw(ArgumentError("format must be :auto, :json, or :yaml")) + stripped = strip(source) + bytes = UInt8[] + retrieval = _inline_id(base_uri) + hint = format + if startswith(stripped, "http://") || startswith(stripped, "https://") + Base.get_extension(@__MODULE__, :OpenAPIHTTPExt) === nothing && throw( + ArgumentError("reading an OpenAPI document from a URL requires `using HTTP`"), + ) + requested = Resources.ResourceId(stripped) + fetched = fetchresource(requested, max_bytes) + bytes = getfield(fetched, :bytes) + retrieval = fetched.id + hint === :auto && + (hint = _format_from_hint(something(fetched.media_type, stripped))) + elseif _isfile(stripped) + file_size = filesize(stripped) + file_size <= max_bytes || + throw(ArgumentError("OpenAPI source exceeds the $max_bytes-byte input limit")) + bytes = Base.read(stripped) + retrieval = _file_id(stripped) + hint === :auto && (hint = _format_from_hint(stripped)) + else + bytes = Vector{UInt8}(codeunits(source)) + end + length(bytes) <= max_bytes || + throw(ArgumentError("OpenAPI source exceeds the $max_bytes-byte input limit")) + isempty(bytes) && throw(ArgumentError("OpenAPI source is empty")) + return bytes, retrieval, _sniff_format(bytes, hint) +end + +function _isfile(source::AbstractString) + (startswith(source, '{') || startswith(source, '[')) && return false + (occursin('\n', source) || occursin('\r', source) || occursin('\0', source)) && + return false + return try + isfile(source) + catch error + error isa IOError || error isa SystemError || rethrow() + false + end +end + +function _yaml_mapping(constructor, node) + return YAML.construct_mapping( + JSON.Object{String,Any}, + constructor, + node; + strict_unique_keys = true, + ) +end + +function _yaml_string(constructor, node) + return string(YAML.construct_scalar(constructor, node)) +end + +function _parse_yaml(text::AbstractString) + constructors = Dict{String,Function}( + "tag:yaml.org,2002:map" => _yaml_mapping, + "tag:yaml.org,2002:timestamp" => _yaml_string, + ) + return YAML.load(text, constructors) +end + +function _normalize_value( + value, + pointer::Resources.JSONPointer, + active::IdDict{Any,Nothing}, + count::Base.RefValue{Int}, + max_nodes::Int, + max_depth::Int, + depth::Int = 0, +) + depth <= max_depth || throw(ArgumentError("OpenAPI source exceeds the depth limit")) + count[] += 1 + count[] <= max_nodes || throw(ArgumentError("OpenAPI source exceeds the node limit")) + if value isa AbstractDict || value isa AbstractVector + haskey(active, value) && + throw(ArgumentError("OpenAPI source contains an alias cycle")) + active[value] = nothing + end + try + if value isa AbstractDict + normalized = JSON.Object{String,Any}() + sizehint!(normalized, length(value)) + for (key, child) in value + key isa AbstractString || throw( + ArgumentError( + "OpenAPI object key at $(string(pointer)) is not a string", + ), + ) + name = String(key) + normalized[name] = _normalize_value( + child, + pointer / name, + active, + count, + max_nodes, + max_depth, + depth + 1, + ) + end + return normalized + elseif value isa AbstractVector + normalized = Any[] + sizehint!(normalized, length(value)) + for (index, child) in enumerate(value) + push!( + normalized, + _normalize_value( + child, + pointer / string(index - 1), + active, + count, + max_nodes, + max_depth, + depth + 1, + ), + ) + end + return normalized + elseif value === nothing || + value isa Bool || + value isa Integer || + value isa AbstractString + return value + elseif value isa AbstractFloat + isfinite(value) || throw( + ArgumentError( + "OpenAPI source contains a non-finite number at $(string(pointer))", + ), + ) + return value + end + throw( + ArgumentError( + "OpenAPI source contains non-JSON value $(typeof(value)) at $(string(pointer))", + ), + ) + finally + (value isa AbstractDict || value isa AbstractVector) && delete!(active, value) + end +end + +function _parse_source( + bytes::AbstractVector{UInt8}, + format::Symbol; + max_nodes::Integer, + max_depth::Integer, + source_locations::Bool = false, +) + max_nodes > 0 || throw(ArgumentError("max_nodes must be positive")) + max_depth > 0 || throw(ArgumentError("max_depth must be positive")) + text = String(copy(bytes)) + parsed = if format === :json + JSON.parse(text; duplicate_keys = :error) + elseif format === :yaml + _parse_yaml(text) + else + throw(ArgumentError("format must be :auto, :json, or :yaml")) + end + normalized = _normalize_value( + parsed, + Resources.JSONPointer(), + IdDict{Any,Nothing}(), + Ref(0), + Int(max_nodes), + Int(max_depth), + ) + source_locations || return normalized + locations = format === :json ? _json_locations(bytes, Int(max_depth)) : + _yaml_locations(text) + return normalized, locations +end + +function _document_version(root, retrieval, bag::DiagnosticBag) + root isa AbstractDict || begin + location = SourceLocation(retrieval) + _error!(bag, :root_type, "the OpenAPI document root must be an object", location) + _throw_on_errors("Cannot load OpenAPI document", bag.diagnostics) + end + declared = get(root, "openapi", nothing) + declared isa AbstractString || begin + location = SourceLocation(retrieval, Resources.JSONPointer("/openapi")) + _error!( + bag, + :missing_version, + "required field `openapi` is missing or is not a string", + location, + ) + _throw_on_errors("Cannot load OpenAPI document", bag.diagnostics) + end + try + return DocumentVersion(declared) + catch error + location = SourceLocation(retrieval, Resources.JSONPointer("/openapi")) + _error!(bag, :unsupported_version, sprint(showerror, error), location) + _throw_on_errors("Cannot load OpenAPI document", bag.diagnostics) + end +end + +function _canonical_document_id(root, retrieval, version, bag) + version.minor == 2 || return retrieval + self = get(root, "\$self", nothing) + self === nothing && return retrieval + self isa AbstractString || begin + _error!( + bag, + :invalid_self, + "`\$self` must be a URI-reference string", + SourceLocation(retrieval, Resources.JSONPointer("/\$self")), + ) + return retrieval + end + try + return Resources.Reference(retrieval, self).resource + catch error + _error!( + bag, + :invalid_self, + sprint(showerror, error), + SourceLocation(retrieval, Resources.JSONPointer("/\$self")), + ) + return retrieval + end +end + +function _load_document( + source::AbstractString, + bag::DiagnosticBag; + base_uri = nothing, + format::Symbol = :auto, + max_bytes::Integer = 16 * 1024 * 1024, + max_nodes::Integer = 1_000_000, + max_depth::Integer = 512, +) + bytes, retrieval, detected = _source_bytes(source, bag; base_uri, format, max_bytes) + root, locations = try + _parse_source( + bytes, + detected; + max_nodes, + max_depth, + source_locations = true, + ) + catch error + _error!( + bag, + :parse_error, + sprint(showerror, error), + SourceLocation( + retrieval; + position = _parse_error_position(error, bytes), + ), + ) + _throw_on_errors("Cannot parse OpenAPI document", bag.diagnostics) + end + version = _document_version(root, retrieval, bag) + canonical = _canonical_document_id(root, retrieval, version, bag) + resource = Resources.Resource( + canonical, + root; + retrieval, + media_type = detected === :json ? "application/openapi+json" : + "application/openapi+yaml", + ) + return SourceDocument( + resource, + version, + detected, + locations, + ) +end + +function _load_document( + source::AbstractDict, + bag::DiagnosticBag; + base_uri = nothing, + max_nodes::Integer = 1_000_000, + max_depth::Integer = 512, + kwargs..., +) + retrieval = _inline_id(base_uri) + root = _normalize_value( + source, + Resources.JSONPointer(), + IdDict{Any,Nothing}(), + Ref(0), + Int(max_nodes), + Int(max_depth), + ) + version = _document_version(root, retrieval, bag) + canonical = _canonical_document_id(root, retrieval, version, bag) + resource = Resources.Resource(canonical, root; retrieval) + return SourceDocument( + resource, + version, + :memory, + Dict{Resources.JSONPointer,SourcePosition}(), + ) +end + +_load_document(source::SourceDocument, bag::DiagnosticBag; kwargs...) = source + +function _structural_diagnostics!(bag::DiagnosticBag, document::SourceDocument) + schema = _spec_schema(document.version.minor) + issues = try + SchemaEngine.validate( + schema, + document.resource.contents; + fail_fast = false, + max_issues = bag.max_diagnostics, + ) + catch error + _error!(bag, :validation_failure, sprint(showerror, error), location(document)) + return bag + end + for issue in issues + pointer = try + Resources.JSONPointer(issue.path) + catch + Resources.JSONPointer() + end + message = "fails the `$(issue.reason)` constraint" + _error!(bag, :spec_schema, message, location(document, pointer)) + end + return bag +end + +""" + OpenAPI.load(source; options...) -> SourceDocument + +Parse JSON or YAML, enforce resource limits, detect OAS 3.0/3.1/3.2, and run +the official structural schema for that OAS minor line. +""" +function load(source; max_diagnostics::Integer = 1_000, validate::Bool = true, kwargs...) + bag = DiagnosticBag(max_diagnostics) + document = _load_document(source, bag; kwargs...) + validate && _structural_diagnostics!(bag, document) + _throw_on_errors("Invalid OpenAPI document", bag.diagnostics) + return document +end + +"""Return all structural diagnostics without throwing for validation errors.""" +function check(source; max_diagnostics::Integer = 1_000, kwargs...) + bag = DiagnosticBag(max_diagnostics) + try + document = _load_document(source, bag; kwargs...) + _structural_diagnostics!(bag, document) + catch error + error isa OpenAPIError || rethrow() + isempty(bag.diagnostics) && append!(bag.diagnostics, error.diagnostics) + end + return copy(bag.diagnostics) +end diff --git a/src/normalize.jl b/src/normalize.jl new file mode 100644 index 0000000..3e41fd3 --- /dev/null +++ b/src/normalize.jl @@ -0,0 +1,1898 @@ +struct Provenance + node::Resources.NodeId + reference_chain::Tuple{Vararg{Resources.NodeId}} +end + +Provenance(object::BoundObject) = Provenance(object.node, object.reference_chain) +Provenance(node::Resources.NodeId) = Provenance(node, (node,)) + +mutable struct SchemaWorkspace + compiled::Any +end + +struct SchemaHandle + node::Resources.NodeId + value::Union{AbstractDict,Bool} + version::DocumentVersion + workspace::SchemaWorkspace +end + +function Base.getproperty(handle::SchemaHandle, name::Symbol) + if name === :compiled + workspace = getfield(handle, :workspace) + workspace.compiled === nothing && return nothing + return SchemaEngine.select(workspace.compiled, getfield(handle, :node)) + end + return getfield(handle, name) +end + +struct NormalizedServerVariable + name::String + default::String + values::Tuple{Vararg{String}} + description::Union{Nothing,String} +end + +struct NormalizedServer + name::Union{Nothing,String} + url::String + description::Union{Nothing,String} + variables::Tuple{Vararg{NormalizedServerVariable}} + provenance::Provenance +end + +struct NormalizedSecurityScheme + name::String + type::Symbol + location::Union{Nothing,Symbol} + parameter_name::Union{Nothing,String} + scheme::Union{Nothing,String} + bearer_format::Union{Nothing,String} + openid_connect_url::Union{Nothing,String} + flows::Any + description::Union{Nothing,String} + extensions::Resources.FrozenObject + provenance::Provenance +end + +struct NormalizedSecurityRequirement + alternatives::Tuple{Vararg{Pair{String,Tuple{Vararg{String}}}}} + provenance::Provenance +end + +struct NormalizedEncoding + name::String + content_type::Union{Nothing,String} + # This is a Tuple rather than Tuple{Vararg{NormalizedHeader}} because + # Header Object content can contain Media Type Objects, which in turn can + # contain Encoding Objects. The abstract field breaks that Julia type + # declaration cycle while the normalizer still stores only + # NormalizedHeader values. + headers::Tuple + # OAS 3.2 permits recursive, named Encoding Objects. Tuple breaks the + # recursive Julia declaration while retaining immutable normalized values. + encoding::Tuple + style::Union{Nothing,Symbol} + explode::Union{Nothing,Bool} + allow_reserved::Bool + raw::Resources.FrozenObject + provenance::Provenance +end + +struct NormalizedMediaType + content_type::String + schema::Union{Nothing,SchemaHandle} + item_schema::Union{Nothing,SchemaHandle} + encoding::Tuple{Vararg{NormalizedEncoding}} + example::Any + examples::Resources.FrozenObject + raw::Resources.FrozenObject + provenance::Provenance +end + +struct NormalizedParameter + name::String + location::Symbol + description::Union{Nothing,String} + required::Bool + deprecated::Bool + allow_empty_value::Bool + style::Union{Nothing,Symbol} + explode::Union{Nothing,Bool} + allow_reserved::Bool + schema::Union{Nothing,SchemaHandle} + content::Tuple{Vararg{NormalizedMediaType}} + example::Any + examples::Resources.FrozenObject + extensions::Resources.FrozenObject + provenance::Provenance +end + +struct NormalizedHeader + name::String + description::Union{Nothing,String} + deprecated::Bool + required::Bool + style::Symbol + explode::Bool + schema::Union{Nothing,SchemaHandle} + content::Tuple{Vararg{NormalizedMediaType}} + provenance::Provenance +end + +struct NormalizedRequestBody + description::Union{Nothing,String} + required::Bool + content::Tuple{Vararg{NormalizedMediaType}} + extensions::Resources.FrozenObject + provenance::Provenance +end + +struct NormalizedResponse + selector::String + summary::Union{Nothing,String} + description::Union{Nothing,String} + headers::Tuple{Vararg{NormalizedHeader}} + content::Tuple{Vararg{NormalizedMediaType}} + links::Resources.FrozenObject + extensions::Resources.FrozenObject + provenance::Provenance +end + +struct NormalizedOperation + id::String + method::Symbol + path::String + direction::Symbol + summary::Union{Nothing,String} + description::Union{Nothing,String} + tags::Tuple{Vararg{String}} + deprecated::Bool + parameters::Tuple{Vararg{NormalizedParameter}} + request_body::Union{Nothing,NormalizedRequestBody} + responses::Tuple{Vararg{NormalizedResponse}} + security::Tuple{Vararg{NormalizedSecurityRequirement}} + servers::Tuple{Vararg{NormalizedServer}} + callbacks::Resources.FrozenObject + extensions::Resources.FrozenObject + provenance::Provenance +end + +struct NormalizedAPI + source::SourceDocument + registry::Resources.FrozenRegistry + title::String + api_version::String + description::Union{Nothing,String} + servers::Tuple{Vararg{NormalizedServer}} + security_schemes::Tuple{Vararg{NormalizedSecurityScheme}} + security::Tuple{Vararg{NormalizedSecurityRequirement}} + schemas::Tuple{Vararg{Pair{String,SchemaHandle}}} + operations::Tuple{Vararg{NormalizedOperation}} + extensions::Resources.FrozenObject + diagnostics::Tuple{Vararg{Diagnostic}} +end + +mutable struct NormalizationContext + resolver::ResolverContext + schema_workspace::SchemaWorkspace + schema_cache::Dict{Resources.NodeId,SchemaHandle} + operation_ids::Dict{String,Resources.NodeId} + callback_operations::Vector{NormalizedOperation} +end + +_frozen_empty() = Resources.freeze(JSON.Object{String,Any}()) + +function _raw_object(value) + value isa AbstractDict || return _frozen_empty() + return Resources.freeze(value) +end + +function _extensions(value) + output = JSON.Object{String,Any}() + value isa AbstractDict || return Resources.freeze(output) + for (key, item) in value + startswith(lowercase(String(key)), "x-") || continue + output[String(key)] = item + end + return Resources.freeze(output) +end + +_optional_string(value, key) = get(value, key, nothing) isa AbstractString ? + String(value[key]) : nothing + +function _schema_handle!( + context::NormalizationContext, + value, + node::Resources.NodeId, +) + (value isa AbstractDict || value isa Bool) || begin + _reference_error!( + context.resolver, + :schema_type, + "Schema Object must be an object or boolean schema", + node, + ) + return nothing + end + cached = get(context.schema_cache, node, nothing) + cached === nothing || return cached + version = _resource_version(context.resolver, node.resource) + handle = SchemaHandle(node, value, version, context.schema_workspace) + context.schema_cache[node] = handle + return handle +end + +function _schema_dialect(context::NormalizationContext, handle::SchemaHandle) + handle.version.minor == 0 && return SchemaEngine.DRAFT4 + resource = Resources.resource(context.resolver.registry, handle.node.resource) + declared = resource.contents isa AbstractDict ? + get(resource.contents, "jsonSchemaDialect", nothing) : nothing + if declared isa AbstractString + normalized = rstrip(String(declared), '#') + if startswith(normalized, "https://spec.openapis.org/oas/3.1/dialect/") || + startswith(normalized, "https://spec.openapis.org/oas/3.2/dialect/") + return SchemaEngine.DRAFT202012 + end + return normalized + end + return SchemaEngine.DRAFT202012 +end + +function _oas_dialect_aliases(resources) + aliases = Dict{String,SchemaEngine.Dialect}() + function visit(value) + if value isa AbstractDict + for key in ("\$schema", "jsonSchemaDialect") + declared = get(value, key, nothing) + declared isa AbstractString || continue + normalized = rstrip(String(declared), '#') + if startswith(normalized, "https://spec.openapis.org/oas/3.1/dialect/") || + startswith(normalized, "https://spec.openapis.org/oas/3.2/dialect/") + aliases[normalized] = SchemaEngine.DRAFT202012 + end + end + foreach(visit, values(value)) + elseif value isa AbstractVector + foreach(visit, value) + end + return + end + for resource in resources + visit(resource.contents) + end + return aliases +end + +function _json_copy(value) + if value isa AbstractDict + output = JSON.Object{String,Any}() + sizehint!(output, length(value)) + for (key, child) in value + output[String(key)] = _json_copy(child) + end + return output + elseif value isa AbstractVector + return Any[_json_copy(child) for child in value] + end + return value +end + +const OAS30_SINGLE_SCHEMA_KEYWORDS = ( + "not", + "items", + "additionalProperties", + "additionalItems", + "propertyNames", + "contains", +) +const OAS30_ARRAY_SCHEMA_KEYWORDS = ("allOf", "anyOf", "oneOf") +const OAS30_MAP_SCHEMA_KEYWORDS = ( + "properties", + "patternProperties", + "definitions", +) +const OAS30_ANNOTATION_KEYWORDS = Set([ + "title", + "description", + "default", + "example", + "deprecated", + "readOnly", + "writeOnly", + "xml", + "externalDocs", + "discriminator", +]) + +"""Translate only OAS 3.0 Schema Objects to their Draft 4 equivalent.""" +function _oas30_schema_compat( + value; + permissive_nullable::Bool = false, + legacy_nullable::Base.RefValue{Bool} = Ref(false), +) + value isa AbstractDict || return _json_copy(value) + output = _json_copy(value) + nullable = get(value, "nullable", false) === true + type = get(value, "type", nothing) + nullable && type isa AbstractString && + (output["type"] = Any[String(type), "null"]) + + for keyword in OAS30_SINGLE_SCHEMA_KEYWORDS + child = get(value, keyword, nothing) + (child isa AbstractDict || child isa Bool) || continue + output[keyword] = _oas30_schema_compat( + child; + permissive_nullable, + legacy_nullable, + ) + end + for keyword in OAS30_ARRAY_SCHEMA_KEYWORDS + children = get(value, keyword, nothing) + children isa AbstractVector || continue + output[keyword] = Any[ + (child isa AbstractDict || child isa Bool) ? + _oas30_schema_compat( + child; + permissive_nullable, + legacy_nullable, + ) : _json_copy(child) for child in children + ] + end + for keyword in OAS30_MAP_SCHEMA_KEYWORDS + children = get(value, keyword, nothing) + children isa AbstractDict || continue + mapped = JSON.Object{String,Any}() + for (name, child) in children + mapped[String(name)] = + (child isa AbstractDict || child isa Bool) ? + _oas30_schema_compat( + child; + permissive_nullable, + legacy_nullable, + ) : _json_copy(child) + end + output[keyword] = mapped + end + dependencies = get(value, "dependencies", nothing) + if dependencies isa AbstractDict + mapped = JSON.Object{String,Any}() + for (name, child) in dependencies + mapped[String(name)] = child isa AbstractDict || child isa Bool ? + _oas30_schema_compat( + child; + permissive_nullable, + legacy_nullable, + ) : _json_copy(child) + end + output["dependencies"] = mapped + end + if nullable && type === nothing && permissive_nullable + legacy_nullable[] = true + assertion = _json_copy(output) + delete!(assertion, "nullable") + wrapped = JSON.Object{String,Any}() + for (key, child) in output + name = String(key) + if name in OAS30_ANNOTATION_KEYWORDS || startswith(lowercase(name), "x-") + wrapped[name] = _json_copy(child) + delete!(assertion, name) + end + end + null_schema = JSON.Object{String,Any}() + null_schema["type"] = "null" + wrapped["anyOf"] = Any[assertion, null_schema] + return wrapped + end + return output +end + +function _oas30_schema_roots!(context::NormalizationContext) + roots = Set( + handle.node for handle in values(context.schema_cache) if + handle.version.minor == 0 + ) + queue = collect(roots) + scanned = Set{Resources.NodeId}() + index = 1 + while index <= length(queue) + node = queue[index] + index += 1 + node in scanned && continue + push!(scanned, node) + resource = Resources.resource(context.resolver.registry, node.resource) + value = try + Resources.resolve(resource.contents, node.pointer) + catch + continue + end + value isa AbstractDict || continue + reference = get(value, "\$ref", nothing) + if reference isa AbstractString + resolved = try + _resolve_reference!(context.resolver, node, reference) + catch + nothing + end + if resolved !== nothing && + (resolved.value isa AbstractDict || resolved.value isa Bool) + push!(roots, resolved.id) + push!(queue, resolved.id) + end + continue + end + for keyword in OAS30_SINGLE_SCHEMA_KEYWORDS + child = get(value, keyword, nothing) + (child isa AbstractDict || child isa Bool) || continue + push!(queue, _childnode(node, keyword)) + end + for keyword in OAS30_ARRAY_SCHEMA_KEYWORDS + children = get(value, keyword, nothing) + children isa AbstractVector || continue + for (child_index, child) in enumerate(children) + (child isa AbstractDict || child isa Bool) || continue + push!( + queue, + _childnode(_childnode(node, keyword), string(child_index - 1)), + ) + end + end + for keyword in OAS30_MAP_SCHEMA_KEYWORDS + children = get(value, keyword, nothing) + children isa AbstractDict || continue + for (name, child) in children + (child isa AbstractDict || child isa Bool) || continue + push!(queue, _childnode(_childnode(node, keyword), String(name))) + end + end + dependencies = get(value, "dependencies", nothing) + if dependencies isa AbstractDict + for (name, child) in dependencies + (child isa AbstractDict || child isa Bool) || continue + push!( + queue, + _childnode(_childnode(node, "dependencies"), String(name)), + ) + end + end + end + return roots +end + +function _replace_pointer(document, pointer::Resources.JSONPointer, value) + isempty(pointer) && return value + parent_pointer = Resources.JSONPointer(Base.front(pointer.tokens)) + parent = Resources.resolve(document, parent_pointer) + token = pointer.tokens[end] + if parent isa AbstractDict + parent[token] = value + else + parent[Base.parse(Int, token) + 1] = value + end + return document +end + +function _schema_resources(context::NormalizationContext) + roots = _oas30_schema_roots!(context) + resources = collect(values(getfield(context.resolver.registry, :resources))) + return Resources.Resource[ + if _resource_version(context.resolver, resource.id).minor == 0 + contents = _json_copy(resource.contents) + resource_roots = sort( + [ + node for node in roots if Resources.resource( + context.resolver.registry, + node.resource, + ).id == resource.id + ]; + by = node -> -length(node.pointer), + ) + for node in resource_roots + schema = try + Resources.resolve(contents, node.pointer) + catch + continue + end + legacy_nullable = Ref(false) + contents = _replace_pointer( + contents, + node.pointer, + _oas30_schema_compat( + schema; + permissive_nullable = !context.resolver.strict, + legacy_nullable, + ), + ) + legacy_nullable[] && _warning!( + context.resolver.bag, + :legacy_nullable_without_type, + "OAS 3.0 nullable has no normative effect without type in the same Schema Object; permissive mode treats it as accepting null", + SourceLocation(node.resource, node.pointer), + ) + end + Resources.Resource( + resource.id, + contents; + retrieval = resource.retrieval, + source = resource.source, + media_type = resource.media_type, + ) + else + resource + end for resource in resources + ] +end + +function _canonical_json!(io::IO, value) + if value isa AbstractDict + print(io, '{') + names = sort!(String[String(key) for key in keys(value)]) + for (index, name) in enumerate(names) + index == 1 || print(io, ',') + print(io, JSON.json(name), ':') + _canonical_json!(io, value[name]) + end + print(io, '}') + elseif value isa AbstractVector + print(io, '[') + for (index, child) in enumerate(value) + index == 1 || print(io, ',') + _canonical_json!(io, child) + end + print(io, ']') + else + print(io, JSON.json(value)) + end + return io +end + +function _content_digest(value) + io = IOBuffer() + _canonical_json!(io, value) + return bytes2hex(SHA.sha256(take!(io))) +end + +function _schema_source_node(registry, node::Resources.NodeId) + resource = Resources.resource(registry, node.resource) + pointer = resource.source.pointer + for token in node.pointer + pointer /= token + end + return Resources.NodeId(resource.source.resource, pointer) +end + +function _top_schema_resource(registry, id::Resources.ResourceId) + resource = Resources.resource(registry, id) + return Resources.resource(registry, resource.source.resource) +end + +function _portable_schema_ids(context::NormalizationContext, schemas) + template = getfield(schemas, :template) + registry = template.registry + resources = collect(values(getfield(registry, :resources))) + top = unique( + resource -> resource.id, + Resources.Resource[ + _top_schema_resource(registry, resource.id) for resource in resources + ], + ) + primary = _top_schema_resource( + registry, + context.resolver.root.resource.retrieval, + ) + labels = Dict{Resources.ResourceId,String}( + primary.id => "root-" * first(_content_digest(primary.contents), 20), + ) + + references = collect(getfield(template, :references)) + while true + candidates = Tuple{String,String,String,Resources.ResourceId}[] + for ((node, keyword), target) in references + source = _schema_source_node(registry, node) + source_top = _top_schema_resource(registry, source.resource) + target_top = _top_schema_resource(registry, target.resource) + haskey(labels, source_top.id) || continue + haskey(labels, target_top.id) && continue + push!( + candidates, + ( + labels[source_top.id], + string(source.pointer), + keyword, + target_top.id, + ), + ) + end + isempty(candidates) && break + sort!(candidates; by = item -> (item[1], item[2], item[3])) + progressed = false + for (parent, pointer, keyword, target) in candidates + haskey(labels, target) && continue + seed = parent * "|" * pointer * "|" * keyword + labels[target] = "external-" * first(bytes2hex(SHA.sha256(seed)), 20) + progressed = true + end + progressed || break + end + + remaining = sort( + [resource for resource in top if !haskey(labels, resource.id)]; + by = resource -> (_content_digest(resource.contents), string(resource.id)), + ) + used = Set(values(labels)) + for resource in remaining + base = "resource-" * first(_content_digest(resource.contents), 20) + label = base + suffix = 2 + while label in used + label = base * "-" * string(suffix) + suffix += 1 + end + labels[resource.id] = label + push!(used, label) + end + + output = Dict{Resources.ResourceId,Resources.ResourceId}() + for resource in resources + owner = _top_schema_resource(registry, resource.id) + label = labels[owner.id] + if resource.id == owner.id + uri = "https://openapi.invalid/schema/" * label * ".json" + else + pointer = string(resource.source.pointer) + suffix = first(bytes2hex(SHA.sha256(pointer)), 20) + uri = "https://openapi.invalid/schema/" * label * "/" * suffix * ".json" + end + output[resource.id] = Resources.ResourceId(uri) + end + return output +end + +function _compile_schemas!(context::NormalizationContext) + isempty(context.schema_cache) && return + handles = sort( + collect(values(context.schema_cache)); + by = handle -> (string(handle.node.resource), string(handle.node.pointer)), + ) + roots = Resources.NodeId[handle.node for handle in handles] + root_dialects = Dict( + handle.node => _schema_dialect(context, handle) for handle in handles + ) + resources = _schema_resources(context) + dialect_aliases = _oas_dialect_aliases(resources) + compiled = try + SchemaEngine.CompiledSchemas( + resources, + roots; + dialect = SchemaEngine.DRAFT202012, + root_dialects, + dialect_aliases, + retriever = _schema_retriever(context.resolver), + max_resources = context.resolver.max_resources, + max_nodes = context.resolver.max_nodes, + max_depth = context.resolver.max_depth, + ) + catch error + location = error isa SchemaEngine.CompilationError ? error.location : first(roots) + _reference_error!( + context.resolver, + :invalid_schema, + sprint(showerror, error), + location, + ) + return + end + context.schema_workspace.compiled = try + SchemaEngine.rebase(compiled, _portable_schema_ids(context, compiled)) + catch error + location = error isa SchemaEngine.CompilationError ? error.location : first(roots) + _reference_error!( + context.resolver, + :invalid_schema_rebase, + "cannot create a portable schema graph: $(sprint(showerror, error))", + location, + ) + return + end + return +end + +function _normalize_servers!(context::NormalizationContext, value, node) + value === nothing && return NormalizedServer[] + output = NormalizedServer[] + names = Set{String}() + for (index, raw) in enumerate(value) + itemnode = _childnode(node, string(index - 1)) + object = _bind_object!(context.resolver, raw, itemnode, :server) + object === nothing && continue + url = get(object.value, "url", nothing) + url isa AbstractString || continue + name = _optional_string(object.value, "name") + if name !== nothing + name in names && _reference_error!( + context.resolver, + :duplicate_server_name, + "server name $(repr(name)) is not unique in this server list", + object.node, + ) + push!(names, name) + end + variables = NormalizedServerVariable[] + raw_variables = get(object.value, "variables", nothing) + if raw_variables isa AbstractDict + variable_node = fieldnode(object, "variables") + for (name, raw_variable) in raw_variables + raw_variable isa AbstractDict || continue + default = get(raw_variable, "default", nothing) + default isa AbstractString || continue + values = get(raw_variable, "enum", String[]) + normalized_values = Tuple(String(item) for item in values) + String(default) in normalized_values || isempty(normalized_values) || + _reference_error!( + context.resolver, + :server_variable_default, + "server variable $(repr(name)) default is not in its enum", + _childnode(variable_node, String(name)), + ) + push!( + variables, + NormalizedServerVariable( + String(name), + String(default), + normalized_values, + _optional_string(raw_variable, "description"), + ), + ) + end + end + placeholders = Set( + String(captures[1]) for captures in + eachmatch(r"\{([^{}]+)\}", String(url)) + ) + defined = Set(variable.name for variable in variables) + for missing in setdiff(placeholders, defined) + _reference_error!( + context.resolver, + :missing_server_variable, + "server URL variable $(repr(missing)) has no definition", + object.node, + ) + end + for unused in setdiff(defined, placeholders) + _warning!( + context.resolver.bag, + :unused_server_variable, + "server variable $(repr(unused)) is not present in the URL template", + SourceLocation(object.node.resource, object.node.pointer), + ) + end + expanded = String(url) + for variable in variables + expanded = replace( + expanded, + "{" * variable.name * "}" => variable.default, + ) + end + parsed = try + Resources.URIs.URI(expanded) + catch error + _reference_error!( + context.resolver, + :invalid_server_url, + "invalid server URL: $(sprint(showerror, error))", + object.node, + ) + nothing + end + if parsed !== nothing && (!isempty(parsed.query) || !isempty(parsed.fragment)) + _reference_error!( + context.resolver, + :invalid_server_url, + "server URL must not contain a query or fragment", + object.node, + ) + end + push!( + output, + NormalizedServer( + name, + String(url), + _optional_string(object.value, "description"), + Tuple(variables), + Provenance(object), + ), + ) + end + return output +end + +function _normalize_security_scheme!(context, name, raw, node) + object = _bind_object!(context.resolver, raw, node, :security_scheme) + object === nothing && return nothing + raw_type = get(object.value, "type", nothing) + raw_type isa AbstractString || return nothing + type = Symbol(replace(lowercase(String(raw_type)), "-" => "_")) + raw_location = _optional_string(object.value, "in") + location = raw_location === nothing ? nothing : Symbol(raw_location) + return NormalizedSecurityScheme( + String(name), + type, + location, + _optional_string(object.value, "name"), + _optional_string(object.value, "scheme"), + _optional_string(object.value, "bearerFormat"), + _optional_string(object.value, "openIdConnectUrl"), + get(object.value, "flows", nothing), + _optional_string(object.value, "description"), + _extensions(object.value), + Provenance(object), + ) +end + +function _normalize_security_schemes!(context::NormalizationContext, value, node) + output = NormalizedSecurityScheme[] + value isa AbstractDict || return output + for (name, raw) in value + itemnode = _childnode(node, String(name)) + scheme = _normalize_security_scheme!(context, name, raw, itemnode) + scheme === nothing || push!(output, scheme) + end + return output +end + +function _normalize_security!(context::NormalizationContext, value, node) + value === nothing && return NormalizedSecurityRequirement[] + output = NormalizedSecurityRequirement[] + for (index, raw) in enumerate(value) + itemnode = _childnode(node, string(index - 1)) + raw isa AbstractDict || continue + alternatives = Pair{String,Tuple{Vararg{String}}}[] + for (name, scopes) in raw + push!(alternatives, String(name) => Tuple(String(scope) for scope in scopes)) + end + push!( + output, + NormalizedSecurityRequirement(Tuple(alternatives), Provenance(itemnode)), + ) + end + return output +end + +function _default_style(location::Symbol) + location === :query && return :form + location === :querystring && return nothing + location === :cookie && return :form + return :simple +end + +function _default_explode(style) + style === nothing && return nothing + return style in (:form, :cookie) +end + +function _normalize_encoding!(context::NormalizationContext, value, node) + output = NormalizedEncoding[] + value isa AbstractDict || return output + for (name, raw) in value + itemnode = _childnode(node, String(name)) + raw isa AbstractDict || continue + raw_style = _optional_string(raw, "style") + style = raw_style === nothing ? nothing : Symbol(raw_style) + headers = NormalizedHeader[] + raw_headers = get(raw, "headers", nothing) + if raw_headers isa AbstractDict + headernode = _childnode(itemnode, "headers") + seen = Set{String}() + for (header_name, raw_header) in raw_headers + lowered = lowercase(String(header_name)) + if lowered == "content-type" + _warning!( + context.resolver.bag, + :ignored_content_type_header, + "encoding header `Content-Type` is defined by `contentType` and is ignored", + SourceLocation( + itemnode.resource, + _childnode(headernode, String(header_name)).pointer, + ), + ) + continue + end + lowered in seen && _reference_error!( + context.resolver, + :duplicate_header, + "encoding header $(repr(header_name)) duplicates a case-insensitive name", + _childnode(headernode, String(header_name)), + ) + push!(seen, lowered) + header = _normalize_header!( + context, + header_name, + raw_header, + _childnode(headernode, String(header_name)), + ) + header === nothing || push!(headers, header) + end + end + push!( + output, + NormalizedEncoding( + String(name), + _optional_string(raw, "contentType"), + Tuple(headers), + Tuple( + _normalize_encoding!( + context, + get(raw, "encoding", nothing), + _childnode(itemnode, "encoding"), + ), + ), + style, + get(raw, "explode", nothing), + get(raw, "allowReserved", false) === true, + _raw_object(raw), + Provenance(itemnode), + ), + ) + end + return output +end + +const MEDIA_TYPE_TOKEN = raw"[!#$%&'*+.^_`|~0-9A-Za-z-]+" + +function _valid_media_type_key(value::AbstractString) + base = strip(first(split(String(value), ';'; limit = 2))) + matched = match( + Regex("^(\\*|" * MEDIA_TYPE_TOKEN * ")/(\\*|\\*\\+" * + MEDIA_TYPE_TOKEN * "|" * MEDIA_TYPE_TOKEN * ")\$"), + base, + ) + matched === nothing && return false + matched.captures[1] == "*" && return matched.captures[2] == "*" + return true +end + +function _normalized_encoding_content_type(content_type) + content_type === nothing && return "" + selected = strip(first(split(String(content_type), ','; limit = 2))) + return lowercase(strip(first(split(selected, ';'; limit = 2)))) +end + +function _check_encoding_scope!(context, encodings, base_media_type) + if startswith(base_media_type, "multipart/") && + base_media_type != "multipart/form-data" + for encoding in encodings + if encoding.style !== nothing || + encoding.explode !== nothing || + haskey(encoding.raw, "allowReserved") + _warning!( + context.resolver.bag, + :ignored_non_form_multipart_encoding_style, + "encoding style, explode, and allowReserved fields are ignored for multipart media types other than multipart/form-data", + SourceLocation( + encoding.provenance.node.resource, + encoding.provenance.node.pointer, + ), + ) + end + end + elseif base_media_type == "application/x-www-form-urlencoded" + for encoding in encodings + isempty(encoding.headers) && continue + _warning!( + context.resolver.bag, + :ignored_form_encoding_headers, + "application/x-www-form-urlencoded encoding headers are ignored", + SourceLocation( + encoding.provenance.node.resource, + encoding.provenance.node.pointer, + ), + ) + end + end + for encoding in encodings + isempty(encoding.encoding) && continue + _check_encoding_scope!( + context, + encoding.encoding, + _normalized_encoding_content_type(encoding.content_type), + ) + end + return +end + +function _normalize_content!(context::NormalizationContext, value, node) + output = NormalizedMediaType[] + value isa AbstractDict || return output + seen = Set{String}() + for (content_type, raw) in value + itemnode = _childnode(node, String(content_type)) + _valid_media_type_key(String(content_type)) || _reference_error!( + context.resolver, + :invalid_media_type, + "content key $(repr(content_type)) is not a valid media type or media range", + itemnode, + ) + normalized_content_type = lowercase( + strip(first(split(String(content_type), ';'; limit = 2))), + ) + normalized_content_type in seen && _reference_error!( + context.resolver, + :duplicate_media_type, + "content key $(repr(content_type)) duplicates a case-insensitive media type", + itemnode, + ) + push!(seen, normalized_content_type) + object = _bind_object!(context.resolver, raw, itemnode, :media_type) + object === nothing && continue + schema = haskey(object.value, "schema") ? + _schema_handle!( + context, + object.value["schema"], + fieldnode(object, "schema"), + ) : + nothing + item_schema = haskey(object.value, "itemSchema") ? + _schema_handle!( + context, + object.value["itemSchema"], + fieldnode(object, "itemSchema"), + ) : nothing + base_media_type = lowercase(strip(first(split(String(content_type), ';'; limit = 2)))) + encodings = _normalize_encoding!( + context, + get(object.value, "encoding", nothing), + fieldnode(object, "encoding"), + ) + supports_named_encoding = + base_media_type == "application/x-www-form-urlencoded" || + startswith(base_media_type, "multipart/") + if !supports_named_encoding && !isempty(encodings) + _warning!( + context.resolver.bag, + :ignored_media_encoding, + "the encoding field is ignored for media type $(repr(content_type))", + SourceLocation(object.node.resource, object.node.pointer), + ) + empty!(encodings) + else + _check_encoding_scope!(context, encodings, base_media_type) + end + push!( + output, + NormalizedMediaType( + String(content_type), + schema, + item_schema, + Tuple(encodings), + get(object.value, "example", nothing), + _raw_object(get(object.value, "examples", nothing)), + _raw_object(object.value), + Provenance(object), + ), + ) + end + return output +end + +function _normalize_parameter!(context::NormalizationContext, raw, node) + object = _bind_object!(context.resolver, raw, node, :parameter) + object === nothing && return nothing + name = get(object.value, "name", nothing) + raw_location = get(object.value, "in", nothing) + (name isa AbstractString && raw_location isa AbstractString) || return nothing + location = Symbol(raw_location) + if location === :header && + lowercase(String(name)) in ("accept", "content-type", "authorization") + _warning!( + context.resolver.bag, + :ignored_header_parameter, + "header parameter $(repr(name)) is reserved by OpenAPI and is ignored", + SourceLocation(object.node.resource, object.node.pointer), + ) + return nothing + end + required = get(object.value, "required", false) === true + if location === :path && !required + _reference_error!( + context.resolver, + :path_parameter_required, + "path parameter $(repr(name)) must set `required: true`", + object.node, + ) + required = true + end + raw_style = _optional_string(object.value, "style") + style = raw_style === nothing ? _default_style(location) : Symbol(raw_style) + explode = get(object.value, "explode", nothing) + explode === nothing && (explode = _default_explode(style)) + allowed_styles = if location === :path + (:matrix, :label, :simple) + elseif location === :query + (:form, :spaceDelimited, :pipeDelimited, :deepObject) + elseif location === :header + (:simple,) + elseif location === :cookie + version = _resource_version(context.resolver, object.node.resource) + version.minor >= 2 ? (:form, :cookie) : (:form,) + elseif location === :querystring + () + else + () + end + if style !== nothing && !(style in allowed_styles) + _reference_error!( + context.resolver, + :parameter_style, + "style $(repr(style)) is not valid for a $(location) parameter", + object.node, + ) + end + schema = haskey(object.value, "schema") ? + _schema_handle!( + context, + object.value["schema"], + fieldnode(object, "schema"), + ) : nothing + content = Tuple( + _normalize_content!( + context, + get(object.value, "content", nothing), + fieldnode(object, "content"), + ), + ) + if (schema === nothing) == isempty(content) + _reference_error!( + context.resolver, + :parameter_shape, + "parameter $(repr(name)) must define exactly one of `schema` or `content`", + object.node, + ) + end + length(content) <= 1 || _reference_error!( + context.resolver, + :parameter_content_count, + "parameter `content` must contain exactly one media type", + fieldnode(object, "content"), + ) + return NormalizedParameter( + String(name), + location, + _optional_string(object.value, "description"), + required, + get(object.value, "deprecated", false) === true, + get(object.value, "allowEmptyValue", false) === true, + style, + explode, + get(object.value, "allowReserved", false) === true, + schema, + content, + get(object.value, "example", nothing), + _raw_object(get(object.value, "examples", nothing)), + _extensions(object.value), + Provenance(object), + ) +end + +function _parameter_key(parameter::NormalizedParameter) + name = parameter.location === :header ? lowercase(parameter.name) : parameter.name + return (name, parameter.location) +end + +function _normalize_parameters!(context::NormalizationContext, value, node) + output = NormalizedParameter[] + seen = Dict{Tuple{String,Symbol},Int}() + value === nothing && return output + for (index, raw) in enumerate(value) + itemnode = _childnode(node, string(index - 1)) + parameter = _normalize_parameter!(context, raw, itemnode) + parameter === nothing && continue + key = _parameter_key(parameter) + if haskey(seen, key) + _reference_error!( + context.resolver, + :duplicate_parameter, + "duplicate parameter $(repr(parameter.name)) in $(parameter.location)", + itemnode, + ) + continue + end + seen[key] = length(output) + 1 + push!(output, parameter) + end + return output +end + +function _merge_parameters(path_parameters, operation_parameters) + output = copy(path_parameters) + positions = Dict(_parameter_key(item) => index for (index, item) in enumerate(output)) + for item in operation_parameters + key = _parameter_key(item) + position = get(positions, key, 0) + if position == 0 + push!(output, item) + positions[key] = length(output) + else + output[position] = item + end + end + return output +end + +function _normalize_header!(context::NormalizationContext, name, raw, node) + object = _bind_object!(context.resolver, raw, node, :header) + object === nothing && return nothing + raw_style = _optional_string(object.value, "style") + style = raw_style === nothing ? :simple : Symbol(raw_style) + explode = get(object.value, "explode", style === :form) + schema = haskey(object.value, "schema") ? + _schema_handle!( + context, + object.value["schema"], + fieldnode(object, "schema"), + ) : nothing + content = Tuple( + _normalize_content!( + context, + get(object.value, "content", nothing), + fieldnode(object, "content"), + ), + ) + if (schema === nothing) == isempty(content) + _reference_error!( + context.resolver, + :header_shape, + "header $(repr(name)) must define exactly one of `schema` or `content`", + object.node, + ) + end + length(content) <= 1 || _reference_error!( + context.resolver, + :header_content_count, + "header `content` must contain exactly one media type", + fieldnode(object, "content"), + ) + style === :simple || _reference_error!( + context.resolver, + :header_style, + "header style must be `simple`", + object.node, + ) + return NormalizedHeader( + String(name), + _optional_string(object.value, "description"), + get(object.value, "deprecated", false) === true, + get(object.value, "required", false) === true, + style, + explode === true, + schema, + content, + Provenance(object), + ) +end + +function _normalize_request_body!(context::NormalizationContext, raw, node) + object = _bind_object!(context.resolver, raw, node, :request_body) + object === nothing && return nothing + content = Tuple( + _normalize_content!( + context, + get(object.value, "content", nothing), + fieldnode(object, "content"), + ), + ) + return NormalizedRequestBody( + _optional_string(object.value, "description"), + get(object.value, "required", false) === true, + content, + _extensions(object.value), + Provenance(object), + ) +end + +function _normalize_response!(context::NormalizationContext, selector, raw, node) + object = _bind_object!(context.resolver, raw, node, :response) + object === nothing && return nothing + headers = NormalizedHeader[] + raw_headers = get(object.value, "headers", nothing) + if raw_headers isa AbstractDict + headernode = fieldnode(object, "headers") + seen = Set{String}() + for (name, raw_header) in raw_headers + lowered = lowercase(String(name)) + if lowered == "content-type" + _warning!( + context.resolver.bag, + :ignored_content_type_header, + "response header `Content-Type` is defined by the response content map and is ignored", + SourceLocation( + object.node.resource, + _childnode(headernode, String(name)).pointer, + ), + ) + continue + end + lowered in seen && _reference_error!( + context.resolver, + :duplicate_header, + "response header names are case-insensitive and $(repr(name)) is duplicated", + _childnode(headernode, String(name)), + ) + push!(seen, lowered) + header = _normalize_header!( + context, + name, + raw_header, + _childnode(headernode, String(name)), + ) + header === nothing || push!(headers, header) + end + end + return NormalizedResponse( + String(selector), + _optional_string(object.value, "summary"), + _optional_string(object.value, "description"), + Tuple(headers), + Tuple( + _normalize_content!( + context, + get(object.value, "content", nothing), + fieldnode(object, "content"), + ), + ), + _raw_object(get(object.value, "links", nothing)), + _extensions(object.value), + Provenance(object), + ) +end + +function _normalize_responses!(context::NormalizationContext, value, node) + output = NormalizedResponse[] + value isa AbstractDict || begin + version = _resource_version(context.resolver, node.resource) + version.minor < 2 && _reference_error!( + context.resolver, + :missing_responses, + "Operation Object must contain a `responses` object", + node, + ) + return output + end + seen = Set{String}() + for (selector, raw) in value + startswith(lowercase(String(selector)), "x-") && continue + normalized = uppercase(String(selector)) + valid = lowercase(normalized) == "default" || + occursin(r"^[1-5][0-9][0-9]$", normalized) || + occursin(r"^[1-5]XX$", normalized) + valid || _reference_error!( + context.resolver, + :invalid_response_selector, + "invalid response selector $(repr(selector))", + _childnode(node, String(selector)), + ) + normalized in seen && _reference_error!( + context.resolver, + :duplicate_response_selector, + "duplicate response selector $(repr(selector))", + _childnode(node, String(selector)), + ) + push!(seen, normalized) + response = _normalize_response!( + context, + selector, + raw, + _childnode(node, String(selector)), + ) + response === nothing || push!(output, response) + end + isempty(seen) && _reference_error!( + context.resolver, + :empty_responses, + "Responses Object must contain at least one response selector", + node, + ) + return output +end + +function _path_parameters!(context, path, parameters, node) + captures = String[String(match.captures[1]) for match in eachmatch(r"\{([^{}]+)\}", path)] + defined = String[ + parameter.name for parameter in parameters if parameter.location === :path + ] + for missing in setdiff(captures, defined) + _reference_error!( + context.resolver, + :missing_path_parameter, + "path template parameter $(repr(missing)) has no matching path parameter", + node, + ) + end + for unused in setdiff(defined, captures) + _reference_error!( + context.resolver, + :unused_path_parameter, + "path parameter $(repr(unused)) is not present in the path template", + node, + ) + end + return +end + +function _operation_id!(context::NormalizationContext, value, method, path, node) + raw = get(value, "operationId", nothing) + id = raw isa AbstractString && !isempty(raw) ? String(raw) : + string(lowercase(String(method)), '_', replace(path, r"[^A-Za-z0-9]+" => "_")) + if haskey(context.operation_ids, id) + firstnode = context.operation_ids[id] + _reference_error!( + context.resolver, + :duplicate_operation_id, + "operationId $(repr(id)) is also used at $(string(firstnode.resource))#$(string(firstnode.pointer))", + node, + ) + else + context.operation_ids[id] = node + end + return id +end + +function _normalize_operation!( + context::NormalizationContext, + raw, + node, + method, + path, + direction, + path_parameters, + inherited_security, + inherited_servers, +) + object = _bind_object!(context.resolver, raw, node, :operation) + object === nothing && return nothing + own_parameters = _normalize_parameters!( + context, + get(object.value, "parameters", nothing), + fieldnode(object, "parameters"), + ) + parameters = _merge_parameters(path_parameters, own_parameters) + query_count = count(parameter -> parameter.location === :query, parameters) + querystring_count = count( + parameter -> parameter.location === :querystring, + parameters, + ) + querystring_count <= 1 || _reference_error!( + context.resolver, + :querystring_parameter_count, + "an operation can contain at most one querystring parameter", + object.node, + ) + (query_count == 0 || querystring_count == 0) || _reference_error!( + context.resolver, + :query_parameter_conflict, + "query and querystring parameters cannot appear in the same operation", + object.node, + ) + direction === :request && _path_parameters!(context, path, parameters, object.node) + request_body = haskey(object.value, "requestBody") ? + _normalize_request_body!( + context, + object.value["requestBody"], + fieldnode(object, "requestBody"), + ) : nothing + responses = _normalize_responses!( + context, + get(object.value, "responses", nothing), + fieldnode(object, "responses"), + ) + security = haskey(object.value, "security") ? + _normalize_security!( + context, + object.value["security"], + fieldnode(object, "security"), + ) : copy(inherited_security) + servers = haskey(object.value, "servers") ? + _normalize_servers!( + context, + object.value["servers"], + fieldnode(object, "servers"), + ) : copy(inherited_servers) + tags = get(object.value, "tags", String[]) + operation = NormalizedOperation( + _operation_id!(context, object.value, method, path, object.node), + method, + String(path), + direction, + _optional_string(object.value, "summary"), + _optional_string(object.value, "description"), + Tuple(String(tag) for tag in tags), + get(object.value, "deprecated", false) === true, + Tuple(parameters), + request_body, + Tuple(responses), + Tuple(security), + Tuple(servers), + _raw_object(get(object.value, "callbacks", nothing)), + _extensions(object.value), + Provenance(object), + ) + raw_callbacks = get(object.value, "callbacks", nothing) + if raw_callbacks isa AbstractDict + callbacks_node = fieldnode(object, "callbacks") + for (name, raw_callback) in raw_callbacks + callback_node = _childnode(callbacks_node, String(name)) + callback = _bind_object!( + context.resolver, + raw_callback, + callback_node, + :callback, + ) + callback === nothing && continue + append!( + context.callback_operations, + _normalize_paths!( + context, + callback.value, + callback.node, + :callback, + security, + servers, + ), + ) + end + end + return operation +end + +const NORMALIZED_METHODS = ( + "get", + "put", + "post", + "delete", + "options", + "head", + "patch", + "trace", + "query", +) + +function _normalize_path_item!( + context::NormalizationContext, + raw, + node, + path, + direction, + inherited_security, + inherited_servers, +) + object = _bind_object!(context.resolver, raw, node, :path_item) + object === nothing && return NormalizedOperation[] + parameters = _normalize_parameters!( + context, + get(object.value, "parameters", nothing), + fieldnode(object, "parameters"), + ) + servers = haskey(object.value, "servers") ? + _normalize_servers!( + context, + object.value["servers"], + fieldnode(object, "servers"), + ) : inherited_servers + output = NormalizedOperation[] + for method_name in NORMALIZED_METHODS + haskey(object.value, method_name) || continue + if method_name == "query" + version = _resource_version(context.resolver, object.node.resource) + version.minor < 2 && continue + end + operation = _normalize_operation!( + context, + object.value[method_name], + fieldnode(object, method_name), + Symbol(uppercase(method_name)), + path, + direction, + parameters, + inherited_security, + servers, + ) + operation === nothing || push!(output, operation) + end + version = _resource_version(context.resolver, object.node.resource) + additional = get(object.value, "additionalOperations", nothing) + if version.minor >= 2 && additional isa AbstractDict + additional_node = fieldnode(object, "additionalOperations") + fixed = Set(uppercase.(NORMALIZED_METHODS)) + for (method_name, raw_operation) in additional + normalized_method = uppercase(String(method_name)) + if normalized_method in fixed + _reference_error!( + context.resolver, + :duplicate_http_method, + "additionalOperations duplicates fixed method $(repr(method_name))", + _childnode(additional_node, String(method_name)), + ) + continue + end + operation = _normalize_operation!( + context, + raw_operation, + _childnode(additional_node, String(method_name)), + Symbol(String(method_name)), + path, + direction, + parameters, + inherited_security, + servers, + ) + operation === nothing || push!(output, operation) + end + end + return output +end + +function _normalize_paths!( + context::NormalizationContext, + value, + node, + direction, + inherited_security, + inherited_servers, +) + output = NormalizedOperation[] + value isa AbstractDict || return output + templates = Dict{String,String}() + for (path, raw) in value + direction === :request && startswith(lowercase(String(path)), "x-") && continue + startswith(String(path), "/") || direction !== :request || _reference_error!( + context.resolver, + :path_prefix, + "path template $(repr(path)) must start with `/`", + _childnode(node, String(path)), + ) + if direction === :request + signature = replace(String(path), r"\{[^{}]+\}" => "{}") + previous = get(templates, signature, nothing) + if previous !== nothing && previous != String(path) + message = "path template $(repr(path)) is equivalent to $(repr(previous))" + if context.resolver.strict + _reference_error!( + context.resolver, + :ambiguous_path_template, + message, + _childnode(node, String(path)), + ) + else + _warning!( + context.resolver.bag, + :ambiguous_path_template, + message * "; both operations are retained in permissive mode", + SourceLocation( + node.resource, + _childnode(node, String(path)).pointer, + ), + ) + end + else + templates[signature] = String(path) + end + end + append!( + output, + _normalize_path_item!( + context, + raw, + _childnode(node, String(path)), + String(path), + direction, + inherited_security, + inherited_servers, + ), + ) + end + return output +end + +function _normalize_component_schemas!(context::NormalizationContext, value, node) + output = Pair{String,SchemaHandle}[] + value isa AbstractDict || return output + for (name, raw) in value + handle = _schema_handle!(context, raw, _childnode(node, String(name))) + handle === nothing || push!(output, String(name) => handle) + end + return output +end + +function _oauth_scopes(scheme::NormalizedSecurityScheme) + output = Set{String}() + scheme.flows isa AbstractDict || return output + for flow in values(scheme.flows) + flow isa AbstractDict || continue + scopes = get(flow, "scopes", nothing) + scopes isa AbstractDict || continue + union!(output, String.(keys(scopes))) + end + return output +end + +function _validate_security_references!(context, requirements, schemes) + known = Dict(scheme.name => scheme for scheme in schemes) + for requirement in requirements + for (name, scopes) in requirement.alternatives + scheme = get(known, name, nothing) + version = _resource_version( + context.resolver, + requirement.provenance.node.resource, + ) + if scheme === nothing && version.minor >= 2 + resolved = try + _resolve_reference!( + context.resolver, + requirement.provenance.node, + name, + ) + catch + nothing + end + if resolved !== nothing + scheme = _normalize_security_scheme!( + context, + name, + resolved.value, + resolved.id, + ) + if scheme !== nothing + known[name] = scheme + push!(schemes, scheme) + end + end + end + scheme === nothing && begin + _reference_error!( + context.resolver, + :unknown_security_scheme, + "security requirement refers to unknown scheme $(repr(name))", + requirement.provenance.node, + ) + continue + end + if version.minor < 2 && + !(scheme.type in (:oauth2, :openidconnect, :open_id_connect)) && + !isempty(scopes) + _reference_error!( + context.resolver, + :invalid_security_scopes, + "security scheme $(repr(name)) does not accept OAuth scopes", + requirement.provenance.node, + ) + elseif scheme.type === :oauth2 + missing = setdiff(Set(scopes), _oauth_scopes(scheme)) + isempty(missing) || _reference_error!( + context.resolver, + :unknown_oauth_scope, + "security requirement uses undefined OAuth scopes $(join(repr.(sort!(collect(missing))), ", "))", + requirement.provenance.node, + ) + end + end + end + return +end + +""" + OpenAPI.normalize(source; options...) -> NormalizedAPI + +Load, resolve, and normalize an OpenAPI 3.0, 3.1, or 3.2 description into a +stable intermediate representation. Strict mode rejects undefined Path Item +reference sibling behavior and all semantic errors before code generation. +""" +function normalize( + source; + strict::Bool = true, + max_diagnostics::Integer = 1_000, + base_uri = nothing, + format::Symbol = :auto, + retriever::Union{Nothing,Resources.AbstractRetriever} = nothing, + file_roots::AbstractVector{<:AbstractString} = String[], + allow_remote_refs::Bool = false, + max_resources::Integer = 256, + max_bytes::Integer = 16 * 1024 * 1024, + max_nodes::Integer = 1_000_000, + max_depth::Integer = 512, +) + document = load( + source; + max_diagnostics, + base_uri, + format, + max_bytes, + max_nodes, + max_depth, + ) + bag = DiagnosticBag(max_diagnostics) + resolver = ResolverContext( + document, + bag; + retriever, + strict, + file_roots, + allow_remote_refs, + max_resources, + max_bytes, + max_nodes, + max_depth, + ) + context = NormalizationContext( + resolver, + SchemaWorkspace(nothing), + Dict{Resources.NodeId,SchemaHandle}(), + Dict{String,Resources.NodeId}(), + NormalizedOperation[], + ) + rootnode = Resources.NodeId(document.resource.id, Resources.JSONPointer()) + root = BoundObject(document.resource.contents, rootnode) + info = root.value["info"] + servers = _normalize_servers!( + context, + get(root.value, "servers", nothing), + _childnode(rootnode, "servers"), + ) + security = _normalize_security!( + context, + get(root.value, "security", nothing), + _childnode(rootnode, "security"), + ) + components = get(root.value, "components", nothing) + component_node = _childnode(rootnode, "components") + schemes = _normalize_security_schemes!( + context, + components isa AbstractDict ? get(components, "securitySchemes", nothing) : nothing, + _childnode(component_node, "securitySchemes"), + ) + schemas = _normalize_component_schemas!( + context, + components isa AbstractDict ? get(components, "schemas", nothing) : nothing, + _childnode(component_node, "schemas"), + ) + operations = _normalize_paths!( + context, + get(root.value, "paths", nothing), + _childnode(rootnode, "paths"), + :request, + security, + servers, + ) + if haskey(root.value, "webhooks") + append!( + operations, + _normalize_paths!( + context, + root.value["webhooks"], + _childnode(rootnode, "webhooks"), + :webhook, + security, + servers, + ), + ) + end + append!(operations, context.callback_operations) + _validate_security_references!(context, security, schemes) + for operation in operations + _validate_security_references!(context, operation.security, schemes) + end + _compile_schemas!(context) + _throw_on_errors("Cannot normalize OpenAPI document", bag.diagnostics) + return NormalizedAPI( + document, + Resources.freeze(resolver.registry), + String(info["title"]), + String(info["version"]), + _optional_string(info, "description"), + Tuple(servers), + Tuple(schemes), + Tuple(security), + Tuple(schemas), + Tuple(operations), + _extensions(root.value), + Tuple(copy(bag.diagnostics)), + ) +end diff --git a/src/planning.jl b/src/planning.jl new file mode 100644 index 0000000..95ded2a --- /dev/null +++ b/src/planning.jl @@ -0,0 +1,1554 @@ +struct ModelFieldPlan + name::String + wire_name::String + type::String + required::Bool + nullable::Bool + default::Union{Nothing,String} +end + +struct ModelPlan + name::String + kind::Symbol + fields::Tuple{Vararg{ModelFieldPlan}} + values::Tuple + alias::Union{Nothing,String} + additional_type::Union{Nothing,String} + discriminator::Union{Nothing,String} + discriminator_mapping::Tuple{ + Vararg{Pair{String,Pair{Resources.NodeId,String}}} + } + discriminator_default::Union{Nothing,Pair{Resources.NodeId,String}} + variants::Tuple{Vararg{Pair{Resources.NodeId,String}}} + direction::Symbol + provenance::Provenance +end + +struct ParameterPlan + name::String + wire_name::String + location::Symbol + type::String + required::Bool + style::Union{Nothing,Symbol} + explode::Union{Nothing,Bool} + allow_reserved::Bool + parameter::NormalizedParameter +end + +struct RequestBodyPlan + name::String + type::String + required::Bool + media_types::Tuple{Vararg{Pair{String,String}}} + body::NormalizedRequestBody +end + +struct ResponsePlan + selector::String + media_types::Tuple{Vararg{Pair{String,String}}} + header_types::Tuple{Vararg{Pair{String,String}}} + response::NormalizedResponse +end + +struct OperationPlan + name::String + operation::NormalizedOperation + parameters::Tuple{Vararg{ParameterPlan}} + request_body::Union{Nothing,RequestBodyPlan} + responses::Tuple{Vararg{ResponsePlan}} + return_type::String +end + +struct ClientPlan + api::NormalizedAPI + module_name::String + models::Tuple{Vararg{ModelPlan}} + operations::Tuple{Vararg{OperationPlan}} + diagnostics::Tuple{Vararg{Diagnostic}} + uses_dates::Bool + uses_uuids::Bool + uses_base64::Bool +end + +struct SchemaView + value::Any + node::Resources.NodeId + version::DocumentVersion + compiled::Any +end + +function SchemaView(handle::SchemaHandle) + compiled = handle.compiled + compiled === nothing && + return SchemaView(handle.value, handle.node, handle.version, nothing) + node = compiled.root + resource = Resources.resource(compiled.registry, node.resource) + value = Resources.resolve(resource.contents, node.pointer) + return SchemaView(value, node, handle.version, compiled) +end + +mutable struct PlanningContext + api::NormalizedAPI + bag::DiagnosticBag + names::Dict{Tuple{Resources.NodeId,Symbol},String} + base_names::Dict{Resources.NodeId,String} + used_names::Dict{String,Int} + planned::Set{Tuple{Resources.NodeId,Symbol}} + planning::Set{Tuple{Resources.NodeId,Symbol}} + directional::Dict{Tuple{Resources.NodeId,String},Bool} + nullable::Dict{Resources.NodeId,Bool} + models::Vector{ModelPlan} + uses_dates::Bool + uses_uuids::Bool + uses_base64::Bool +end + +const JULIA_RESERVED_NAMES = Set([ + "baremodule", + "begin", + "break", + "catch", + "const", + "continue", + "do", + "else", + "elseif", + "end", + "export", + "false", + "finally", + "for", + "function", + "global", + "if", + "import", + "let", + "local", + "macro", + "module", + "quote", + "return", + "struct", + "true", + "try", + "using", + "while", + "where", + "mutable", + "primitive", + "abstract", + "type", + "Client", + "ApiError", + "Absent", + "ABSENT", +]) + +const GENERATED_TYPE_NAMES = Set([ + "HTTP", + "JSON", + "OpenAPI", + "SchemaEngine", + "Base64", + "Dates", + "UUIDs", + "Absent", + "DecodeError", + "ApiError", + "ApiResponse", + "UnexpectedBody", + "UnsupportedMediaType", + "UnexpectedContentType", + "SchemaValidationError", + "AbstractCredential", + "ApiKeyCredential", + "BasicCredential", + "BearerCredential", + "HttpCredential", + "MutualTLSCredential", + "Upload", + "MultipartPartHeaders", + "Client", +]) + +function _words(value::AbstractString) + normalized = replace(String(value), r"[^A-Za-z0-9]+" => " ") + return [word for word in split(normalized) if !isempty(word)] +end + +function _type_identifier(value::AbstractString) + words = _words(value) + output = isempty(words) ? "Model" : join(uppercasefirst(word) for word in words) + isdigit(first(output)) && (output = "Model" * output) + symbol = Symbol(output) + if lowercase(output) in JULIA_RESERVED_NAMES || + output in GENERATED_TYPE_NAMES || + isdefined(Core, symbol) || + isdefined(Base, symbol) + output *= "Model" + end + return output +end + +function _field_identifier(value::AbstractString) + words = _words(value) + output = isempty(words) ? "value" : join(lowercase.(words), '_') + isdigit(first(output)) && (output = "value_" * output) + output in JULIA_RESERVED_NAMES && (output *= '_') + Base.isidentifier(output) || (output = "value") + return output +end + +function _allocate_name!(context::PlanningContext, suggested::AbstractString) + base = _type_identifier(suggested) + count = get(context.used_names, base, 0) + 1 + context.used_names[base] = count + return count == 1 ? base : string(base, count) +end + +function _canonical_node(view::SchemaView, node::Resources.NodeId) + view.compiled === nothing && return node + return Resources.canonical(view.compiled.registry, node) +end + +function _child_view(view::SchemaView, tokens...) + node = view.node + value = view.value + for token in tokens + key = String(token) + value = value isa AbstractDict ? value[key] : value[Base.parse(Int, key) + 1] + node = _childnode(node, key) + node = _canonical_node(view, node) + end + return SchemaView(value, node, view.version, view.compiled) +end + +function _reference_target(view::SchemaView) + view.value isa AbstractDict || return nothing + raw = get(view.value, "\$ref", nothing) + raw isa AbstractString || return nothing + view.compiled === nothing && return nothing + target = SchemaEngine.reference_target(view.compiled, view.node) + target === nothing && return nothing + resource = Resources.resource(view.compiled.registry, target.resource) + value = Resources.resolve(resource.contents, target.pointer) + return SchemaView(value, target, view.version, view.compiled) +end + +const NON_APPLICATIVE_SCHEMA_SIBLINGS = Set([ + "\$comment", + "title", + "description", + "default", + "examples", + "example", + "deprecated", + "readOnly", + "writeOnly", + "externalDocs", + "xml", +]) + +function _resolved_view(view::SchemaView) + target = _reference_target(view) + target === nothing && return view + view.version.minor == 0 && return target + siblings = String[String(key) for key in keys(view.value) if key != "\$ref"] + all(key -> key in NON_APPLICATIVE_SCHEMA_SIBLINGS, siblings) && return target + return view +end + +function _keyword_owner( + view::SchemaView, + keyword::AbstractString, + seen = Set{Resources.NodeId}(), +) + view.node in seen && return nothing + push!(seen, view.node) + try + view.value isa AbstractDict || return nothing + haskey(view.value, keyword) && return view + target = _reference_target(view) + if target !== nothing + owner = _keyword_owner(target, keyword, seen) + owner === nothing || return owner + end + allof = get(view.value, "allOf", nothing) + if allof isa AbstractVector + parent = _child_view(view, "allOf") + for index in eachindex(allof) + owner = _keyword_owner( + _child_view(parent, string(index - 1)), + keyword, + seen, + ) + owner === nothing || return owner + end + end + return nothing + finally + delete!(seen, view.node) + end +end + +const DIRECTIONAL_SINGLE_SCHEMA_KEYS = ( + "not", + "if", + "then", + "else", + "items", + "contains", + "additionalProperties", + "unevaluatedProperties", + "propertyNames", + "contentSchema", +) +const DIRECTIONAL_ARRAY_SCHEMA_KEYS = ("allOf", "anyOf", "oneOf", "prefixItems") +const DIRECTIONAL_OBJECT_SCHEMA_KEYS = ( + "properties", + "patternProperties", + "dependentSchemas", +) + +function _directional_view(view::SchemaView) + target = _reference_target(view) + return view.version.minor == 0 && target !== nothing ? target : view +end + +function _directional_children(view::SchemaView) + output = SchemaView[] + value = view.value + value isa AbstractDict || return output + target = _reference_target(view) + target === nothing || push!(output, target) + for key in DIRECTIONAL_SINGLE_SCHEMA_KEYS + raw = get(value, key, nothing) + (raw isa AbstractDict || raw isa Bool) || continue + push!(output, _child_view(view, key)) + end + for key in DIRECTIONAL_ARRAY_SCHEMA_KEYS + raw = get(value, key, nothing) + raw isa AbstractVector || continue + parent = _child_view(view, key) + for index in eachindex(raw) + push!(output, _child_view(parent, string(index - 1))) + end + end + for key in DIRECTIONAL_OBJECT_SCHEMA_KEYS + raw = get(value, key, nothing) + raw isa AbstractDict || continue + parent = _child_view(view, key) + for name in keys(raw) + push!(output, _child_view(parent, String(name))) + end + end + return output +end + +function _populate_directional_cache!( + cache::Dict{Tuple{Resources.NodeId,String},Bool}, + root::SchemaView, + keyword::String, +) + views = Dict{Resources.NodeId,SchemaView}() + reverse_edges = Dict{Resources.NodeId,Vector{Resources.NodeId}}() + known = Dict{Resources.NodeId,Bool}() + stack = SchemaView[root] + while !isempty(stack) + view = _directional_view(pop!(stack)) + node = view.node + haskey(views, node) && continue + views[node] = view + key = (node, keyword) + if haskey(cache, key) + known[node] = cache[key] + continue + end + for raw_child in _directional_children(view) + child = _directional_view(raw_child) + parents = get!(reverse_edges, child.node, Resources.NodeId[]) + node in parents || push!(parents, node) + haskey(views, child.node) || push!(stack, child) + end + end + + reachable = Set{Resources.NodeId}() + queue = Resources.NodeId[] + for (node, view) in views + value = view.value + local_match = value isa AbstractDict && get(value, keyword, false) === true + if local_match || get(known, node, false) + push!(reachable, node) + push!(queue, node) + end + end + while !isempty(queue) + node = pop!(queue) + for parent in get(reverse_edges, node, Resources.NodeId[]) + parent in reachable && continue + push!(reachable, parent) + push!(queue, parent) + end + end + for node in keys(views) + cache[(node, keyword)] = node in reachable + end + return +end + +function _has_directional_property( + view::SchemaView, + keyword::String, + cache::Dict{Tuple{Resources.NodeId,String},Bool} = + Dict{Tuple{Resources.NodeId,String},Bool}(), +) + object = _directional_view(view) + key = (object.node, keyword) + haskey(cache, key) || _populate_directional_cache!(cache, object, keyword) + return cache[key] +end + +function _direction(context::PlanningContext, view::SchemaView, mode::Symbol) + mode === :neutral && return :neutral + mode === :input && + _has_directional_property(view, "readOnly", context.directional) && + return :input + mode === :output && + _has_directional_property(view, "writeOnly", context.directional) && + return :output + return :neutral +end + +function _base_component_names(api::NormalizedAPI) + output = Dict{Resources.NodeId,String}() + for (name, handle) in api.schemas + output[handle.node] = String(name) + if handle.compiled !== nothing + output[handle.compiled.root] = String(name) + output[Resources.canonical(handle.compiled.registry, handle.node)] = String(name) + end + end + return output +end + +function PlanningContext(api::NormalizedAPI, bag::DiagnosticBag) + return PlanningContext( + api, + bag, + Dict{Tuple{Resources.NodeId,Symbol},String}(), + _base_component_names(api), + Dict{String,Int}(), + Set{Tuple{Resources.NodeId,Symbol}}(), + Set{Tuple{Resources.NodeId,Symbol}}(), + Dict{Tuple{Resources.NodeId,String},Bool}(), + Dict{Resources.NodeId,Bool}(), + ModelPlan[], + false, + false, + false, + ) +end + +function _model_name!(context::PlanningContext, view::SchemaView, suggested, mode) + direction = _direction(context, view, mode) + resolved = _resolved_view(view) + key = (resolved.node, direction) + existing = get(context.names, key, nothing) + existing === nothing || return existing, key, resolved + base = get(context.base_names, resolved.node, String(suggested)) + suffix = direction === :input ? "Input" : direction === :output ? "Output" : "" + name = _allocate_name!(context, base * suffix) + context.names[key] = name + return name, key, resolved +end + +function _schema_types(value) + value isa AbstractDict || return String[] + raw = get(value, "type", nothing) + raw isa AbstractString && return [String(raw)] + raw isa AbstractVector && return String[String(item) for item in raw] + return String[] +end + +function _intersect_types(left::Vector{String}, right::Vector{String}) + isempty(left) && return copy(right) + isempty(right) && return copy(left) + return String[type for type in left if type in right] +end + +function _effective_types( + view::SchemaView, + seen = Set{Resources.NodeId}(), +) + view.node in seen && return String[] + push!(seen, view.node) + try + value = view.value + value isa AbstractDict || return String[] + types = _schema_types(value) + target = _reference_target(view) + target === nothing || + (types = _intersect_types(types, _effective_types(target, seen))) + allof = get(value, "allOf", nothing) + if allof isa AbstractVector + parent = _child_view(view, "allOf") + for index in eachindex(allof) + types = _intersect_types( + types, + _effective_types( + _child_view(parent, string(index - 1)), + seen, + ), + ) + end + end + return unique(types) + finally + delete!(seen, view.node) + end +end + +function _structural_nullable(view::SchemaView, seen = Set{Resources.NodeId}()) + view.value === false && return false + view.value === true && return true + view.node in seen && return false + push!(seen, view.node) + value = view.value + try + view.version.minor == 0 && get(value, "nullable", false) === true && return true + "null" in _effective_types(view) && return true + for keyword in ("oneOf", "anyOf") + owner = _keyword_owner(view, keyword) + owner === nothing && continue + alternatives = owner.value[keyword] + alternatives isa AbstractVector || continue + parent = _child_view(owner, keyword) + any(eachindex(alternatives)) do index + alternative = _child_view(parent, string(index - 1)) + return alternative.value isa AbstractDict && + get(alternative.value, "type", nothing) == "null" || + _structural_nullable(alternative, seen) + end && return true + end + target = _reference_target(view) + target === nothing || return _structural_nullable(target, seen) + return false + finally + delete!(seen, view.node) + end +end + +function _nullable(context::PlanningContext, view::SchemaView) + compiled = view.compiled + if compiled !== nothing + node = Resources.canonical(compiled.registry, view.node) + return get!(context.nullable, node) do + Base.isvalid(SchemaEngine.subschema(compiled, node), nothing) + end + end + return _structural_nullable(view) +end + +function _without_null_type(types::Vector{String}) + return [type for type in types if type != "null"] +end + +function _primitive_type!( + context::PlanningContext, + type, + format, + mode::Symbol = :neutral, +) + if type == "integer" + return format == "int32" ? "Int32" : "Int64" + elseif type == "number" + return format == "float" ? "Float32" : "Float64" + elseif type == "boolean" + return "Bool" + elseif type == "null" + return "Nothing" + elseif type == "string" + if format == "date" + context.uses_dates = true + return "Dates.Date" + elseif format == "date-time" + context.uses_dates = true + return "Dates.DateTime" + elseif format == "time" + context.uses_dates = true + return "Dates.Time" + elseif format == "uuid" + context.uses_uuids = true + return "UUIDs.UUID" + elseif format in ("byte", "base64") + context.uses_base64 = true + return "Vector{UInt8}" + elseif format == "binary" + return mode === :input ? "Union{Vector{UInt8},Upload}" : + "Vector{UInt8}" + end + return "String" + end + return "Any" +end + +function _union_string(types) + unique_types = unique(String[type for type in types if type != "Union{}"]) + isempty(unique_types) && return "Any" + "Any" in unique_types && return "Any" + length(unique_types) == 1 && return only(unique_types) + sort!(unique_types) + return "Union{" * join(unique_types, ',') * "}" +end + +function _field_type(base::String, required::Bool, nullable::Bool) + variants = String[base] + nullable && !occursin(r"\bNothing\b", base) && push!(variants, "Nothing") + required || push!(variants, "Absent") + return _union_string(variants) +end + +function _is_object_schema( + view::SchemaView, + seen = Set{Resources.NodeId}(), +) + view.node in seen && return false + push!(seen, view.node) + try + value = view.value + value isa AbstractDict || return false + types = _schema_types(value) + "object" in types && return true + isempty(types) && any( + haskey(value, key) for key in ( + "properties", + "additionalProperties", + "patternProperties", + "unevaluatedProperties", + ) + ) && return true + target = _reference_target(view) + target === nothing || _is_object_schema(target, seen) && return true + allof = get(value, "allOf", nothing) + allof isa AbstractVector || return false + parent = _child_view(view, "allOf") + return any(eachindex(allof)) do index + _is_object_schema(_child_view(parent, string(index - 1)), seen) + end + finally + delete!(seen, view.node) + end +end + +function _combine_additional(left, right) + (left === false || right === false) && return false + left === true && return right + right === true && return left + left_items = left isa AbstractVector ? copy(left) : SchemaView[left] + right_items = right isa AbstractVector ? right : SchemaView[right] + append!(left_items, right_items) + unique!(item -> item.node, left_items) + return left_items +end + +function _local_additional(view::SchemaView) + value = view.value + value isa AbstractDict || return true + patterns = SchemaView[] + raw_patterns = get(value, "patternProperties", nothing) + if raw_patterns isa AbstractDict + parent = _child_view(view, "patternProperties") + for name in keys(raw_patterns) + push!(patterns, _child_view(parent, String(name))) + end + end + keyword = haskey(value, "additionalProperties") ? "additionalProperties" : + haskey(value, "unevaluatedProperties") ? "unevaluatedProperties" : nothing + keyword === nothing && return true + raw = value[keyword] + raw === true && return true + raw === false && return isempty(patterns) ? false : patterns + push!(patterns, _child_view(view, keyword)) + return patterns +end + +function _object_members(view::SchemaView, seen = Set{Resources.NodeId}()) + view.node in seen && + return Tuple{String,SchemaView,Bool,Bool,Bool}[], true + push!(seen, view.node) + members = Tuple{String,SchemaView,Bool,Bool,Bool}[] + additional = true + value = view.value + try + target = _reference_target(view) + if target !== nothing + target_members, target_additional = _object_members(target, seen) + append!(members, target_members) + additional = _combine_additional(additional, target_additional) + view.version.minor == 0 && return target_members, target_additional + end + value isa AbstractDict || return members, additional + required = Set(String.(get(value, "required", String[]))) + properties = get(value, "properties", nothing) + if properties isa AbstractDict + property_node = _child_view(view, "properties") + for (name, raw) in properties + child = _child_view(property_node, String(name)) + read_only = raw isa AbstractDict && get(raw, "readOnly", false) === true + write_only = raw isa AbstractDict && get(raw, "writeOnly", false) === true + push!( + members, + (String(name), child, String(name) in required, read_only, write_only), + ) + end + end + additional = _combine_additional(additional, _local_additional(view)) + allof = get(value, "allOf", nothing) + if allof isa AbstractVector + parent = _child_view(view, "allOf") + for index in eachindex(allof) + child = _child_view(parent, string(index - 1)) + child_members, child_additional = _object_members(child, seen) + append!(members, child_members) + additional = _combine_additional(additional, child_additional) + end + end + finally + delete!(seen, view.node) + end + deduped = Tuple{String,SchemaView,Bool,Bool,Bool}[] + positions = Dict{String,Int}() + for member in members + position = get(positions, member[1], 0) + if position == 0 + push!(deduped, member) + positions[member[1]] = length(deduped) + else + previous = deduped[position] + deduped[position] = ( + member[1], + member[2], + previous[3] || member[3], + previous[4] || member[4], + previous[5] || member[5], + ) + end + end + return deduped, additional +end + +function _plan_enum!(context, view, suggested, mode, values) + name, key, resolved = _model_name!(context, view, suggested, mode) + key in context.planned && return name + key in context.planning && return name + push!(context.planning, key) + value_types = unique(typeof(value) for value in values) + alias = if all(value -> value isa AbstractString, values) + "String" + elseif all(value -> value isa Integer && !(value isa Bool), values) + "Int64" + elseif all(value -> value isa Real && !(value isa Bool), values) + "Float64" + elseif all(value -> value isa Bool, values) + "Bool" + else + "Any" + end + push!( + context.models, + ModelPlan( + name, + :enum, + (), + Tuple(values), + alias, + nothing, + nothing, + (), + nothing, + (), + key[2], + Provenance(resolved.node), + ), + ) + delete!(context.planning, key) + push!(context.planned, key) + return name +end + +function _plan_object!(context, view, suggested, mode) + name, key, resolved = _model_name!(context, view, suggested, mode) + key in context.planned && return name + key in context.planning && return name + push!(context.planning, key) + members, additional = _object_members(resolved) + fields = ModelFieldPlan[] + used_fields = Dict("additional_properties" => 1) + for (wire_name, child, required, read_only, write_only) in members + mode === :input && read_only && continue + mode === :output && write_only && continue + field_name = _field_identifier(wire_name) + count = get(used_fields, field_name, 0) + 1 + used_fields[field_name] = count + count > 1 && (field_name = string(field_name, '_', count)) + base = _type_for!(context, child, name * _type_identifier(wire_name), mode) + nullable = _nullable(context, child) + default = required ? nothing : "ABSENT" + push!( + fields, + ModelFieldPlan( + field_name, + wire_name, + _field_type(base, required, nullable), + required, + nullable, + default, + ), + ) + end + additional_type = if additional === false + nothing + elseif additional isa AbstractVector + _union_string( + _type_for!( + context, + item, + name * "AdditionalValue" * string(index), + mode, + ) for (index, item) in enumerate(additional) + ) + elseif additional isa SchemaView + _type_for!(context, additional, name * "AdditionalValue", mode) + else + "Any" + end + push!( + context.models, + ModelPlan( + name, + :object, + Tuple(fields), + (), + nothing, + additional_type, + nothing, + (), + nothing, + (), + key[2], + Provenance(resolved.node), + ), + ) + delete!(context.planning, key) + push!(context.planned, key) + return name +end + +function _union_alternatives(view::SchemaView, keyword::String) + raw = get(view.value, keyword, nothing) + raw isa AbstractVector || return SchemaView[] + parent = _child_view(view, keyword) + return SchemaView[ + _child_view(parent, string(index - 1)) for index in eachindex(raw) + if !(raw[index] isa AbstractDict && get(raw[index], "type", nothing) == "null") + ] +end + +function _reference_view(view::SchemaView, reference::AbstractString) + view.compiled === nothing && return nothing + resolved = Resources.resolve( + view.compiled.registry, + Resources.Reference(view.node.resource, reference), + ) + (resolved.value isa AbstractDict || resolved.value isa Bool) || return nothing + return SchemaView( + resolved.value, + Resources.canonical(view.compiled.registry, resolved.id), + view.version, + view.compiled, + ) +end + +function _plan_union!(context, view, suggested, mode, keyword) + resolved = _resolved_view(view) + union_owner = something(_keyword_owner(resolved, keyword), resolved) + alternatives = _union_alternatives(union_owner, keyword) + discriminator_owner = _keyword_owner(resolved, "discriminator") + discriminator = discriminator_owner === nothing ? nothing : + discriminator_owner.value["discriminator"] + if !haskey(context.base_names, resolved.node) && + discriminator === nothing && + length(alternatives) == 1 && + _nullable(context, resolved) + base = _type_for!(context, only(alternatives), suggested, mode) + return _union_string((base, "Nothing")) + end + + name, key, resolved = _model_name!(context, view, suggested, mode) + key in context.planned && return name + key in context.planning && return name + push!(context.planning, key) + + alternative_types = String[ + _type_for!(context, alternative, suggested * string(index), mode) for + (index, alternative) in enumerate(alternatives) + ] + types = copy(alternative_types) + _nullable(context, resolved) && push!(types, "Nothing") + property_name = discriminator isa AbstractDict && + get(discriminator, "propertyName", nothing) isa AbstractString ? + String(discriminator["propertyName"]) : nothing + mapping_by_tag = Dict{String,Pair{Resources.NodeId,String}}() + variants = Pair{Resources.NodeId,String}[ + _resolved_view(alternative).node => alternative_types[index] for + (index, alternative) in enumerate(alternatives) + ] + if property_name !== nothing + for (index, alternative) in enumerate(alternatives) + target = _resolved_view(alternative).node + tag = get(context.base_names, target, nothing) + tag === nothing || + (mapping_by_tag[tag] = target => alternative_types[index]) + end + end + if discriminator isa AbstractDict && + get(discriminator, "mapping", nothing) isa AbstractDict + for (tag, reference) in discriminator["mapping"] + reference isa AbstractString || continue + target = try + _reference_view(resolved, reference) + catch error + _error!( + context.bag, + :invalid_discriminator_mapping, + "cannot resolve discriminator mapping $(repr(tag)): $(sprint(showerror, error))", + SourceLocation(resolved.node.resource, resolved.node.pointer), + ) + missing + end + target === missing && continue + if target === nothing + _error!( + context.bag, + :invalid_discriminator_mapping, + "discriminator mapping $(repr(tag)) does not resolve to a schema", + SourceLocation(resolved.node.resource, resolved.node.pointer), + ) + continue + end + target_type = _type_for!( + context, + target, + suggested * _type_identifier(String(tag)), + mode, + ) + push!(types, target_type) + mapping_by_tag[String(tag)] = target.node => target_type + end + end + mapping = sort!(collect(mapping_by_tag); by = first) + + default_mapping = nothing + if discriminator isa AbstractDict && + get(discriminator, "defaultMapping", nothing) isa AbstractString + target = try + _reference_view(resolved, discriminator["defaultMapping"]) + catch error + _error!( + context.bag, + :invalid_discriminator_default, + "cannot resolve discriminator defaultMapping: $(sprint(showerror, error))", + SourceLocation(resolved.node.resource, resolved.node.pointer), + ) + missing + end + if target === missing + nothing + elseif target === nothing + _error!( + context.bag, + :invalid_discriminator_default, + "discriminator defaultMapping does not resolve to a schema", + SourceLocation(resolved.node.resource, resolved.node.pointer), + ) + else + target_type = _type_for!(context, target, suggested * "Default", mode) + push!(types, target_type) + default_mapping = target.node => target_type + end + end + union_type = _union_string(types) + push!( + context.models, + ModelPlan( + name, + keyword == "oneOf" ? :oneof : :anyof, + (), + Tuple(types), + union_type, + nothing, + property_name, + Tuple(mapping), + default_mapping, + Tuple(variants), + key[2], + Provenance(resolved.node), + ), + ) + delete!(context.planning, key) + push!(context.planned, key) + return name +end + +function _schema_keyword(view::SchemaView, keyword::AbstractString, default = nothing) + owner = _keyword_owner(view, keyword) + return owner === nothing ? default : owner.value[keyword] +end + +function _array_type!(context, view::SchemaView, suggested, mode) + prefix_owner = _keyword_owner(view, "prefixItems") + items_owner = _keyword_owner(view, "items") + if prefix_owner !== nothing && + prefix_owner.value["prefixItems"] isa AbstractVector + raw_prefix = prefix_owner.value["prefixItems"] + parent = _child_view(prefix_owner, "prefixItems") + prefix_types = String[ + _type_for!( + context, + _child_view(parent, string(index - 1)), + suggested * string(index), + mode, + ) for index in eachindex(raw_prefix) + ] + raw_items = items_owner === nothing ? true : items_owner.value["items"] + minimum = _schema_keyword(view, "minItems", 0) + maximum = _schema_keyword(view, "maxItems", nothing) + exact = raw_items === false && + minimum isa Integer && minimum == length(prefix_types) && + (maximum === nothing || maximum == length(prefix_types)) + exact && return "Tuple{" * join(prefix_types, ',') * "}" + element_types = copy(prefix_types) + if raw_items === true + push!(element_types, "Any") + elseif raw_items isa AbstractDict || raw_items isa Bool + raw_items === false || push!( + element_types, + _type_for!( + context, + _child_view(items_owner, "items"), + suggested * "Rest", + mode, + ), + ) + end + return "Vector{" * _union_string(element_types) * "}" + end + if items_owner !== nothing + raw_items = items_owner.value["items"] + if raw_items isa AbstractDict || raw_items isa Bool + raw_items === false && return "Vector{Union{}}" + raw_items === true && return "Vector{Any}" + item = _child_view(items_owner, "items") + return "Vector{" * + _type_for!(context, item, suggested * "Item", mode) * "}" + end + end + return "Vector{Any}" +end + +function _primitive_format(view::SchemaView) + format = _schema_keyword(view, "format", nothing) + encoding = _schema_keyword(view, "contentEncoding", nothing) + encoding == "base64" && return "base64" + return format +end + +function _type_for!( + context::PlanningContext, + original::SchemaView, + suggested::AbstractString, + mode::Symbol, +) + view = _resolved_view(original) + value = view.value + value === true && return "Any" + value === false && return "Union{}" + enum = _schema_keyword(view, "enum", nothing) + enum isa AbstractVector && !isempty(enum) && + return _plan_enum!(context, view, suggested, mode, enum) + const_owner = _keyword_owner(view, "const") + if const_owner !== nothing + constant = const_owner.value["const"] + return _primitive_type!( + context, + constant isa Bool ? "boolean" : constant isa Integer ? "integer" : + constant isa Real ? "number" : constant isa AbstractString ? "string" : + constant === nothing ? "null" : "", + nothing, + mode, + ) + end + for keyword in ("oneOf", "anyOf") + _keyword_owner(view, keyword) === nothing || + return _plan_union!(context, view, suggested, mode, keyword) + end + types = _without_null_type(_effective_types(view)) + object_schema = _is_object_schema(view) + if isempty(types) + if object_schema + return _plan_object!(context, view, suggested, mode) + elseif _keyword_owner(view, "items") !== nothing || + _keyword_owner(view, "prefixItems") !== nothing + types = ["array"] + else + return "Any" + end + end + if types == ["object"] + return _plan_object!(context, view, suggested, mode) + end + alias_state = nothing + component_name = get(context.base_names, view.node, nothing) + if component_name !== nothing && !("object" in types) + name, key, resolved = _model_name!(context, view, suggested, mode) + key in context.planned && return name + key in context.planning && return name + push!(context.planning, key) + alias_state = (name, key, resolved) + end + output = String[] + for type in types + if type == "object" + push!(output, _plan_object!(context, view, suggested, mode)) + elseif type == "array" + push!(output, _array_type!(context, view, suggested, mode)) + else + push!( + output, + _primitive_type!( + context, + type, + _primitive_format(view), + mode, + ), + ) + end + end + base = _union_string(output) + if _nullable(context, view) && base != "Nothing" + base = _union_string((base, "Nothing")) + end + if alias_state !== nothing + name, key, resolved = alias_state + push!( + context.models, + ModelPlan( + name, + :alias, + (), + (), + base, + nothing, + nothing, + (), + nothing, + (), + key[2], + Provenance(resolved.node), + ), + ) + delete!(context.planning, key) + push!(context.planned, key) + return name + end + return base +end + +function _schema_type!(context, schema, suggested, mode) + schema === nothing && return "Any" + return _type_for!(context, SchemaView(schema), suggested, mode) +end + +function _media_type!( + context, + media::NormalizedMediaType, + suggested, + mode; + parameter::Bool = false, +) + media.schema === nothing || + return _schema_type!(context, media.schema, suggested, mode) + base = lowercase(strip(first(split(media.content_type, ';'; limit = 2)))) + (base == "application/json" || endswith(base, "+json")) && return "Any" + startswith(base, "text/") && return "String" + parameter && return "String" + return mode === :input ? "Union{String,Vector{UInt8},Upload}" : + "Vector{UInt8}" +end + +function _parameter_schema(parameter::NormalizedParameter) + parameter.schema !== nothing && return parameter.schema + isempty(parameter.content) && return nothing + return first(parameter.content).schema +end + +function _plan_parameters!(context, operation, function_name) + output = ParameterPlan[] + used = Dict( + "body" => 1, + "client" => 1, + "content_type" => 1, + "accept" => 1, + "server" => 1, + "with_http_info" => 1, + "request_headers" => 1, + "request_options" => 1, + "multipart_headers" => 1, + ) + for parameter in operation.parameters + name = _field_identifier(parameter.name) + count = get(used, name, 0) + 1 + used[name] = count + count > 1 && (name = string(name, '_', count)) + parameter_schema = _parameter_schema(parameter) + base = if parameter.schema === nothing && !isempty(parameter.content) + _media_type!( + context, + first(parameter.content), + _type_identifier(function_name) * _type_identifier(parameter.name), + :input; + parameter = true, + ) + else + _schema_type!( + context, + parameter_schema, + _type_identifier(function_name) * _type_identifier(parameter.name), + :input, + ) + end + type = _field_type( + base, + parameter.required, + parameter_schema === nothing ? false : + _nullable(context, SchemaView(parameter_schema)), + ) + push!( + output, + ParameterPlan( + name, + parameter.name, + parameter.location, + type, + parameter.required, + parameter.style, + parameter.explode, + parameter.allow_reserved, + parameter, + ), + ) + end + return output +end + +function _plan_request_body!(context, operation, function_name) + body = operation.request_body + body === nothing && return nothing + media_types = Pair{String,String}[] + for media in body.content + type = _media_type!( + context, + media, + _type_identifier(function_name) * "Request", + :input, + ) + push!(media_types, media.content_type => type) + end + base = isempty(media_types) ? "Any" : _union_string(last.(media_types)) + type = _field_type(base, body.required, false) + return RequestBodyPlan("body", type, body.required, Tuple(media_types), body) +end + +function _plan_responses!(context, operation, function_name) + output = ResponsePlan[] + success_types = String[] + for response in operation.responses + media_types = Pair{String,String}[] + for media in response.content + type = _media_type!( + context, + media, + _type_identifier(function_name) * "Response" * + replace(response.selector, r"[^A-Za-z0-9]" => ""), + :output, + ) + push!(media_types, media.content_type => type) + end + header_types = Pair{String,String}[] + for header in response.headers + schema = header.schema !== nothing ? header.schema : + isempty(header.content) ? nothing : first(header.content).schema + type = _schema_type!( + context, + schema, + _type_identifier(function_name) * + _type_identifier(response.selector) * + _type_identifier(header.name) * "Header", + :output, + ) + push!(header_types, header.name => type) + end + push!( + output, + ResponsePlan( + response.selector, + Tuple(media_types), + Tuple(header_types), + response, + ), + ) + selector = uppercase(response.selector) + if startswith(selector, "2") || selector == "DEFAULT" + if isempty(media_types) + push!(success_types, "Nothing") + else + append!(success_types, last.(media_types)) + end + end + end + if isempty(success_types) + if isempty(operation.responses) + append!(success_types, ("Nothing", "Vector{UInt8}")) + else + push!(success_types, "Nothing") + end + end + return output, _union_string(success_types) +end + +function _plan_operation!(context, operation, used_functions) + base = _field_identifier(operation.id) + count = get(used_functions, base, 0) + 1 + used_functions[base] = count + name = count == 1 ? base : string(base, '_', count) + parameters = _plan_parameters!(context, operation, name) + request = _plan_request_body!(context, operation, name) + responses, return_type = _plan_responses!(context, operation, name) + return OperationPlan( + name, + operation, + Tuple(parameters), + request, + Tuple(responses), + return_type, + ) +end + +function _has_positional_encoding(encoding::NormalizedEncoding) + return haskey(encoding.raw, "itemEncoding") || + haskey(encoding.raw, "prefixEncoding") || + any(_has_positional_encoding, encoding.encoding) +end + +function _encoding_value_schema(view::SchemaView) + types = _without_null_type(_effective_types(view)) + if "array" in types || _keyword_owner(view, "items") !== nothing + owner = _keyword_owner(view, "items") + if owner !== nothing + raw = owner.value["items"] + (raw isa AbstractDict || raw isa Bool) && raw !== false && + return _child_view(owner, "items") + end + end + return view +end + +function _planning_content_base(content_type) + content_type === nothing && return "" + selected = strip(first(split(String(content_type), ','; limit = 2))) + return lowercase(strip(first(split(selected, ';'; limit = 2)))) +end + +function _check_named_encodings!( + context::PlanningContext, + encodings, + schema::Union{Nothing,SchemaView}, +) + members = schema === nothing ? Tuple{String,SchemaView,Bool,Bool,Bool}[] : + first(_object_members(schema)) + known = Dict(member[1] => member[2] for member in members) + for encoding in encodings + property = get(known, encoding.name, nothing) + if property === nothing + _warning!( + context.bag, + :ignored_encoding_property, + "encoding key $(repr(encoding.name)) has no corresponding schema property and is ignored", + SourceLocation( + encoding.provenance.node.resource, + encoding.provenance.node.pointer, + ), + ) + continue + end + isempty(encoding.encoding) && continue + base = _planning_content_base(encoding.content_type) + if !(base == "application/x-www-form-urlencoded" || + startswith(base, "multipart/")) + _error!( + context.bag, + :invalid_nested_encoding_media_type, + "nested named encodings require an explicit multipart or application/x-www-form-urlencoded contentType", + SourceLocation( + encoding.provenance.node.resource, + encoding.provenance.node.pointer, + ), + ) + continue + end + _check_named_encodings!( + context, + encoding.encoding, + _encoding_value_schema(property), + ) + end + return +end + +function _check_generation_support!(context::PlanningContext, strict::Bool) + for operation in context.api.operations + operation.direction === :request || continue + for parameter in operation.parameters + parameter.location === :querystring || continue + _error!( + context.bag, + :unsupported_querystring_generation, + "OAS 3.2 querystring parameter generation is not implemented", + SourceLocation( + parameter.provenance.node.resource, + parameter.provenance.node.pointer, + ), + ) + end + for parameter in operation.parameters + parameter.schema === nothing && continue + view = SchemaView(parameter.schema) + types = _without_null_type(_effective_types(view)) + array_schema = "array" in types || + _keyword_owner(view, "items") !== nothing || + _keyword_owner(view, "prefixItems") !== nothing + if parameter.style === :deepObject && !_is_object_schema(view) + emit = strict ? _error! : _warning! + emit( + context.bag, + :invalid_deep_object_schema, + strict ? + "deepObject serialization requires an object schema" : + "non-object deepObject schema uses non-standard bracket compatibility serialization", + SourceLocation( + parameter.provenance.node.resource, + parameter.provenance.node.pointer, + ), + ) + elseif parameter.style in (:spaceDelimited, :pipeDelimited) && + !array_schema + _error!( + context.bag, + :invalid_delimited_schema, + "$(parameter.style) serialization requires an array schema", + SourceLocation( + parameter.provenance.node.resource, + parameter.provenance.node.pointer, + ), + ) + end + end + media_types = NormalizedMediaType[] + operation.request_body === nothing || + append!(media_types, operation.request_body.content) + for response in operation.responses + append!(media_types, response.content) + end + for media in media_types + streaming = media.item_schema !== nothing || + haskey(media.raw, "itemEncoding") || + haskey(media.raw, "prefixEncoding") || + any( + _has_positional_encoding, + media.encoding, + ) + streaming || continue + _error!( + context.bag, + :unsupported_streaming_generation, + "streaming itemSchema and positional encoding generation is not implemented", + SourceLocation( + media.provenance.node.resource, + media.provenance.node.pointer, + ), + ) + end + if operation.request_body !== nothing + for media in operation.request_body.content + isempty(media.encoding) && continue + _check_named_encodings!( + context, + media.encoding, + media.schema === nothing ? nothing : SchemaView(media.schema), + ) + end + end + end + return +end + +"""Build the deterministic Julia model and operation plan used by code generation.""" +function plan( + source; + name::AbstractString = "ApiClient", + strict::Bool = true, + max_diagnostics::Integer = 1_000, + kwargs..., +) + api = source isa NormalizedAPI ? source : + normalize(source; strict, max_diagnostics, kwargs...) + bag = DiagnosticBag(max_diagnostics) + context = PlanningContext(api, bag) + _check_generation_support!(context, strict) + # Reserve stable component names before recursive planning starts. + for (component_name, handle) in sort(collect(api.schemas); by = first) + view = SchemaView(handle) + resolved = _resolved_view(view) + key = (resolved.node, :neutral) + haskey(context.names, key) || + (context.names[key] = _allocate_name!(context, component_name)) + end + for (component_name, handle) in sort(collect(api.schemas); by = first) + _type_for!(context, SchemaView(handle), component_name, :neutral) + end + operations = OperationPlan[] + used_functions = Dict{String,Int}() + ordered = sort( + [operation for operation in api.operations if operation.direction === :request]; + by = operation -> (operation.path, String(operation.method), operation.id), + ) + for operation in ordered + push!(operations, _plan_operation!(context, operation, used_functions)) + end + diagnostics = Diagnostic[api.diagnostics...] + append!(diagnostics, bag.diagnostics) + _throw_on_errors("Cannot plan Julia client", diagnostics) + # Recursive planning emits dependencies before parents. Sort only aliases and + # enums that do not depend on declaration order; object order stays topological. + return ClientPlan( + api, + _type_identifier(name), + Tuple(context.models), + Tuple(operations), + Tuple(diagnostics), + context.uses_dates, + context.uses_uuids, + context.uses_base64, + ) +end diff --git a/src/read.jl b/src/read.jl new file mode 100644 index 0000000..a9d6077 --- /dev/null +++ b/src/read.jl @@ -0,0 +1,44 @@ +""" + OpenAPI.read(source; options...) -> AbstractDict + +Read a JSON or YAML OpenAPI 3.0, 3.1, or 3.2 document. `source` may be inline +text, a local file, or an HTTP(S) URL when HTTP.jl is loaded. The returned +object is recursively read-only. + +Use [`OpenAPI.load`](@ref) when source identity, format, and version metadata +are also needed. +""" +function read(source::AbstractString; kwargs...) + try + return load(source; kwargs...).resource.contents + catch error + error isa OpenAPIError || rethrow() + throw(ArgumentError(sprint(showerror, error))) + end +end + +function parse(source::AbstractString; kwargs...) + try + return load(source; kwargs...).resource.contents + catch error + error isa OpenAPIError || rethrow() + throw(ArgumentError(sprint(showerror, error))) + end +end + +""" + OpenAPI.validate(document) -> document + +Validate an in-memory document against the official structural schema for its +declared OAS 3.0, 3.1, or 3.2 minor line. This compatibility API returns the +original object. Use [`OpenAPI.check`](@ref) to collect structured diagnostics. +""" +function validate(document::AbstractDict; kwargs...) + try + normalize(document; kwargs...) + catch error + error isa OpenAPIError || rethrow() + throw(ArgumentError(sprint(showerror, error))) + end + return document +end diff --git a/src/references.jl b/src/references.jl new file mode 100644 index 0000000..2430ba8 --- /dev/null +++ b/src/references.jl @@ -0,0 +1,403 @@ +"""A bounded retriever for relative files and explicitly allowed HTTP resources.""" +struct DocumentRetriever <: Resources.AbstractRetriever + file::Union{Nothing,Resources.FileRetriever} + allow_http::Bool + allowed_origins::Tuple{Vararg{String}} + max_bytes::Int +end + +function _origin(id::Resources.ResourceId) + uri = id.uri + scheme = lowercase(uri.scheme) + scheme in ("http", "https") || return nothing + port = isempty(uri.port) ? (scheme == "https" ? "443" : "80") : uri.port + return string(scheme, "://", lowercase(uri.host), ':', port) +end + +function DocumentRetriever( + document::SourceDocument; + file_roots::AbstractVector{<:AbstractString} = String[], + allow_remote_refs::Bool = false, + max_bytes::Integer = 16 * 1024 * 1024, +) + max_bytes > 0 || throw(ArgumentError("max_bytes must be positive")) + roots = String[String(root) for root in file_roots] + retrieval = document.resource.retrieval + if lowercase(retrieval.uri.scheme) == "file" + path = Resources.URIs.unescapeuri(retrieval.uri.path) + pushfirst!(roots, dirname(path)) + end + unique!(roots) + file = isempty(roots) ? nothing : Resources.FileRetriever(roots; max_bytes) + initial_origin = _origin(retrieval) + origins = initial_origin === nothing ? () : (initial_origin,) + return DocumentRetriever(file, allow_remote_refs, origins, Int(max_bytes)) +end + +function Resources.retrieve(retriever::DocumentRetriever, id::Resources.ResourceId) + scheme = lowercase(id.uri.scheme) + if scheme in ("", "file") + retriever.file === nothing && throw( + Resources.RetrievalError(id, "external file retrieval is disabled"), + ) + return Resources.retrieve(retriever.file, id) + elseif scheme in ("http", "https") + origin = _origin(id) + allowed = retriever.allow_http || origin in retriever.allowed_origins + allowed || throw( + Resources.RetrievalError( + id, + "cross-origin HTTP retrieval is disabled; pass allow_remote_refs=true to enable it", + ), + ) + Base.get_extension(@__MODULE__, :OpenAPIHTTPExt) === nothing && throw( + Resources.RetrievalError(id, "HTTP retrieval requires HTTP.jl to be loaded"), + ) + return fetchresource(id, retriever.max_bytes) + end + throw(Resources.RetrievalError(id, "URI scheme $(repr(scheme)) is not allowed")) +end + +struct BoundObject + value::AbstractDict + node::Resources.NodeId + field_nodes::Dict{String,Resources.NodeId} + reference_chain::Tuple{Vararg{Resources.NodeId}} +end + +function BoundObject(value::AbstractDict, node::Resources.NodeId) + return BoundObject( + value, + node, + Dict{String,Resources.NodeId}(), + (node,), + ) +end + +function fieldnode(object::BoundObject, name::AbstractString) + return get(object.field_nodes, String(name), _childnode(object.node, name)) +end + +function _childnode(node::Resources.NodeId, token::AbstractString) + return Resources.NodeId(node.resource, node.pointer / token) +end + +mutable struct ResolverContext{R<:Resources.AbstractRetriever} + root::SourceDocument + registry::Resources.Registry + versions::Dict{Resources.ResourceId,DocumentVersion} + formats::Dict{Resources.ResourceId,Symbol} + retriever::R + bag::DiagnosticBag + strict::Bool + max_resources::Int + max_nodes::Int + max_depth::Int + loaded::Int + resolving::Set{Resources.NodeId} +end + +function ResolverContext( + document::SourceDocument, + bag::DiagnosticBag; + retriever::Union{Nothing,Resources.AbstractRetriever} = nothing, + strict::Bool = true, + file_roots::AbstractVector{<:AbstractString} = String[], + allow_remote_refs::Bool = false, + max_resources::Integer = 256, + max_bytes::Integer = 16 * 1024 * 1024, + max_nodes::Integer = 1_000_000, + max_depth::Integer = 512, +) + max_resources > 0 || throw(ArgumentError("max_resources must be positive")) + selected = something( + retriever, + DocumentRetriever( + document; + file_roots, + allow_remote_refs, + max_bytes, + ), + ) + registry = Resources.Registry() + Resources.register!(registry, document.resource) + return ResolverContext( + document, + registry, + Dict(document.resource.id => document.version), + Dict(document.resource.id => document.format), + selected, + bag, + strict, + Int(max_resources), + Int(max_nodes), + Int(max_depth), + 1, + Set{Resources.NodeId}(), + ) +end + +function _resource_version(context::ResolverContext, id::Resources.ResourceId) + registered = Resources.resource(context.registry, id) + return get(context.versions, registered.id, context.root.version) +end + +function _resource_format(id::Resources.ResourceId, media_type) + media_type !== nothing && return _format_from_hint(media_type) + return _format_from_hint(id.uri.path) +end + +function _register_retrieved!( + context::ResolverContext, + requested::Resources.ResourceId, + retrieved::Resources.RetrievedResource, +) + context.loaded < context.max_resources || throw( + Resources.RetrievalError(requested, "OpenAPI resource limit reached"), + ) + format = _resource_format(retrieved.id, retrieved.media_type) + format = format === :auto ? _sniff_format(getfield(retrieved, :bytes), :auto) : format + root, locations = _parse_source( + getfield(retrieved, :bytes), + format; + max_nodes = context.max_nodes, + max_depth = context.max_depth, + source_locations = true, + ) + retrieval = retrieved.id + version = nothing + canonical = retrieval + if root isa AbstractDict && get(root, "openapi", nothing) isa AbstractString + version = try + DocumentVersion(root["openapi"]) + catch error + throw(Resources.RetrievalError(requested, sprint(showerror, error))) + end + canonical = _canonical_document_id(root, retrieval, version, context.bag) + end + resource = Resources.Resource( + canonical, + root; + retrieval, + media_type = retrieved.media_type, + ) + aliases = requested == retrieval ? Resources.ResourceId[] : [requested] + if haskey(context.registry, canonical) + for alias in (retrieval, requested) + haskey(context.registry, alias) || + Resources.register_alias!(context.registry, alias, canonical) + end + else + Resources.register!(context.registry, resource; aliases) + context.loaded += 1 + end + if version !== nothing + context.versions[canonical] = version + context.formats[canonical] = format + external = SourceDocument( + resource, + version, + format, + locations, + ) + _structural_diagnostics!(context.bag, external) + end + return Resources.resource(context.registry, requested) +end + +function _ensure_resource!(context::ResolverContext, id::Resources.ResourceId) + haskey(context.registry, id) && return Resources.resource(context.registry, id) + retrieved = try + Resources.retrieve(context.retriever, id) + catch error + throw(Resources.RetrievalError(id, sprint(showerror, error))) + end + return _register_retrieved!(context, id, retrieved) +end + +function _resolve_reference!( + context::ResolverContext, + base::Resources.NodeId, + raw::AbstractString, +) + reference = try + Resources.Reference(base.resource, raw) + catch error + throw(ArgumentError("invalid reference $(repr(raw)): $(sprint(showerror, error))")) + end + _ensure_resource!(context, reference.resource) + return Resources.resolve(context.registry, reference) +end + +function _reference_error!(context, code, message, node) + _error!(context.bag, code, message, SourceLocation(node.resource, node.pointer)) + return nothing +end + +function _bind_object!( + context::ResolverContext, + value, + node::Resources.NodeId, + kind::Symbol, +) + value isa AbstractDict || begin + _reference_error!( + context, + :object_type, + "expected an object for $(replace(String(kind), '_' => ' '))", + node, + ) + return nothing + end + raw_reference = get(value, "\$ref", nothing) + raw_reference === nothing && return BoundObject(value, node) + raw_reference isa AbstractString || begin + _reference_error!(context, :invalid_reference, "`\$ref` must be a string", node) + return nothing + end + resolved = try + _resolve_reference!(context, node, raw_reference) + catch error + _reference_error!(context, :unresolved_reference, sprint(showerror, error), node) + return nothing + end + target = resolved.id + target in context.resolving && begin + _reference_error!( + context, + :reference_cycle, + "a non-schema reference cycle reaches $(string(target.resource))#$(string(target.pointer))", + node, + ) + return nothing + end + resolved.value isa AbstractDict || begin + _reference_error!( + context, + :reference_target_type, + "reference target is not an object", + node, + ) + return nothing + end + push!(context.resolving, target) + bound = try + _bind_object!(context, resolved.value, target, kind) + finally + delete!(context.resolving, target) + end + bound === nothing && return nothing + + siblings = String[String(key) for key in keys(value) if key != "\$ref"] + isempty(siblings) && return BoundObject( + bound.value, + bound.node, + bound.field_nodes, + (node, bound.reference_chain...), + ) + referring_version = _resource_version(context, node.resource) + if kind === :path_item + message = "Path Item Object `\$ref` siblings have undefined behavior" + if context.strict + _reference_error!(context, :path_item_reference_siblings, message, node) + return nothing + end + _warning!( + context.bag, + :path_item_reference_siblings, + message * "; local fields override the target in permissive mode", + SourceLocation(node.resource, node.pointer), + ) + merged = JSON.Object{String,Any}() + for (key, item) in bound.value + merged[String(key)] = item + end + field_nodes = copy(bound.field_nodes) + for key in siblings + merged[key] = value[key] + field_nodes[key] = _childnode(node, key) + end + return BoundObject( + merged, + bound.node, + field_nodes, + (node, bound.reference_chain...), + ) + end + + allowed = referring_version.minor == 0 ? Set{String}() : Set(("summary", "description")) + ignored = [key for key in siblings if !(key in allowed)] + isempty(ignored) || _warning!( + context.bag, + :ignored_reference_siblings, + "Reference Object siblings $(join(repr.(ignored), ", ")) are ignored by OAS $(referring_version.major).$(referring_version.minor)", + SourceLocation(node.resource, node.pointer), + ) + overrides = [key for key in siblings if key in allowed] + isempty(overrides) && return BoundObject( + bound.value, + bound.node, + bound.field_nodes, + (node, bound.reference_chain...), + ) + merged = JSON.Object{String,Any}() + for (key, item) in bound.value + merged[String(key)] = item + end + field_nodes = copy(bound.field_nodes) + for key in overrides + merged[key] = value[key] + field_nodes[key] = _childnode(node, key) + end + return BoundObject( + merged, + bound.node, + field_nodes, + (node, bound.reference_chain...), + ) +end + +function _schema_retriever(context::ResolverContext) + return SchemaResourceRetriever( + context.retriever, + context.root.version.minor == 0, + context.root.version.minor == 0 && !context.strict, + context.max_nodes, + context.max_depth, + ) +end + +"""Adapter that converts YAML resources to JSON before the schema engine compiles them.""" +struct SchemaResourceRetriever{R<:Resources.AbstractRetriever} <: + Resources.AbstractRetriever + retriever::R + oas30::Bool + permissive_nullable::Bool + max_nodes::Int + max_depth::Int +end + +function Resources.retrieve(retriever::SchemaResourceRetriever, id::Resources.ResourceId) + resource = Resources.retrieve(retriever.retriever, id) + format = _resource_format(resource.id, resource.media_type) + bytes = getfield(resource, :bytes) + detected = format === :auto ? _sniff_format(bytes, :auto) : format + if retriever.oas30 || detected === :yaml + parsed = _parse_source( + bytes, + detected; + max_nodes = retriever.max_nodes, + max_depth = retriever.max_depth, + ) + retriever.oas30 && (parsed = _oas30_schema_compat( + parsed; + permissive_nullable = retriever.permissive_nullable, + )) + bytes = Vector{UInt8}(codeunits(JSON.json(parsed))) + end + return Resources.RetrievedResource( + resource.id, + bytes; + media_type = "application/schema+json", + ) +end diff --git a/src/runtime.jl b/src/runtime.jl new file mode 100644 index 0000000..43a6d55 --- /dev/null +++ b/src/runtime.jl @@ -0,0 +1,9 @@ +"""Runtime support shared by generated clients.""" +module Runtime + +struct Absent end +const ABSENT = Absent() + +Base.show(io::IO, ::Absent) = print(io, "OpenAPI.Runtime.ABSENT") + +end diff --git a/src/schema_engine/README.md b/src/schema_engine/README.md new file mode 100644 index 0000000..fe26780 --- /dev/null +++ b/src/schema_engine/README.md @@ -0,0 +1,14 @@ +# Provisional schema engine + +This directory contains the generic JSON Schema resource, reference, +compilation, rebasing, and validation code required by OpenAPI.jl and its +generated clients. + +The code is intentionally separate from OpenAPI loading, normalization, +planning, and generation. OpenAPI-specific behavior must not enter this +directory. + +The implementation came from the experimental JSONSchema.jl +`codex/openapi-foundation` branch. Keeping it here allows the API and behavior +to harden with real OpenAPI documents before a possible future move back to +JSONSchema.jl. diff --git a/src/schema_engine/SchemaEngine.jl b/src/schema_engine/SchemaEngine.jl new file mode 100644 index 0000000..79748e2 --- /dev/null +++ b/src/schema_engine/SchemaEngine.jl @@ -0,0 +1,20 @@ +""" +Internal JSON Schema resource, compilation, rebasing, and validation support. + +This module is isolated from OpenAPI-specific semantics so it can move to +JSONSchema.jl after the implementation and API have hardened. Generated clients +use it through `OpenAPI.SchemaEngine`; it is not a general-purpose exported API. +""" +module SchemaEngine + +import JSON +import URIs + +include("resources.jl") +include("dialects.jl") +include("issues.jl") +include("compiled.jl") +include("rebase.jl") +include("compiled_validation.jl") + +end diff --git a/src/schema_engine/compiled.jl b/src/schema_engine/compiled.jl new file mode 100644 index 0000000..de07c6a --- /dev/null +++ b/src/schema_engine/compiled.jl @@ -0,0 +1,1558 @@ +# Copyright (c) 2026: fredo-dedup, quinnj, and contributors +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +struct CompilationError <: Exception + location::Resources.NodeId + reason::String +end + +function Base.showerror(io::IO, err::CompilationError) + pointer = string(err.location.pointer) + location = + string(err.location.resource) * (isempty(pointer) ? "" : "#" * pointer) + return print( + io, + "cannot compile JSON Schema at ", + repr(location), + ": ", + err.reason, + ) +end + +struct PendingReference + base::Resources.ResourceId + keyword::String + reference::String + dialect::Dialect + location::Resources.NodeId +end + +struct CompiledNode + index::Int + id::Resources.NodeId + value::Union{Resources.FrozenObject,Bool} + dialect::Dialect +end + +mutable struct Compiler{R<:Resources.AbstractRetriever} + registry::Resources.Registry + dialects::Dict{Resources.NodeId,Dialect} + dialect_aliases::Dict{String,Dialect} + recursive_anchors::Set{Resources.ResourceId} + references::Dict{Tuple{Resources.NodeId,String},Resources.NodeId} + regexes::Dict{String,Regex} + evaluation_nodes::Dict{Resources.NodeId,CompiledNode} + transitions::Dict{Tuple{Int,Tuple{Vararg{String}}},CompiledNode} + uses_annotations::Bool + retriever::R + pending::Vector{PendingReference} + loaded::Set{Resources.ResourceId} + loading::Set{Resources.ResourceId} + max_resources::Int + max_nodes::Int + max_depth::Int + nodes::Int + values::Int +end + +function Compiler( + retriever::R, + max_resources::Integer, + max_nodes::Integer, + max_depth::Integer, +) where {R<:Resources.AbstractRetriever} + max_resources > 0 || throw(ArgumentError("max_resources must be positive")) + max_nodes > 0 || throw(ArgumentError("max_nodes must be positive")) + max_depth > 0 || throw(ArgumentError("max_depth must be positive")) + return Compiler( + Resources.Registry(), + Dict{Resources.NodeId,Dialect}(), + Dict{String,Dialect}(), + Set{Resources.ResourceId}(), + Dict{Tuple{Resources.NodeId,String},Resources.NodeId}(), + Dict{String,Regex}(), + Dict{Resources.NodeId,CompiledNode}(), + Dict{Tuple{Int,Tuple{Vararg{String}}},CompiledNode}(), + false, + retriever, + PendingReference[], + Set{Resources.ResourceId}(), + Set{Resources.ResourceId}(), + Int(max_resources), + Int(max_nodes), + Int(max_depth), + 0, + 0, + ) +end + +function _register_dialect_aliases!(compiler::Compiler, aliases::AbstractDict) + for (uri, target) in aliases + uri isa AbstractString || + throw(ArgumentError("dialect alias identifiers must be strings")) + compiler.dialect_aliases[_normalized_dialect_uri(uri)] = dialect(target) + end + return compiler +end + +"""A non-mutating, dialect-aware JSON Schema resource graph.""" +struct CompiledSchema{R<:Resources.AbstractRetriever} + data::Union{Resources.FrozenObject,Bool} + dialect::Dialect + registry::Resources.FrozenRegistry + root::Resources.NodeId + dialects::Dict{Resources.NodeId,Dialect} + dialect_aliases::Dict{String,Dialect} + evaluation_nodes::Dict{Resources.NodeId,CompiledNode} + transitions::Dict{Tuple{Int,Tuple{Vararg{String}}},CompiledNode} + uses_annotations::Bool + recursive_anchors::Set{Resources.ResourceId} + references::Dict{Tuple{Resources.NodeId,String},Resources.NodeId} + regexes::Dict{String,Regex} + retriever::R +end + +"""A compiled graph with multiple JSON Schema roots embedded in JSON resources.""" +struct CompiledSchemas{R<:Resources.AbstractRetriever} + template::CompiledSchema{R} + roots::Dict{Resources.NodeId,Resources.NodeId} + + function CompiledSchemas(template::CompiledSchema{R}, roots) where {R} + return new{R}(template, copy(roots)) + end +end + +function Base.getproperty(schemas::CompiledSchemas, name::Symbol) + name === :roots && return copy(getfield(schemas, :roots)) + return getfield(schemas, name) +end + +function Base.getproperty(schema::CompiledSchema, name::Symbol) + name in ( + :dialects, + :dialect_aliases, + :evaluation_nodes, + :transitions, + :references, + :regexes, + ) && return copy(getfield(schema, name)) + name === :recursive_anchors && return copy(getfield(schema, name)) + return getfield(schema, name) +end + +""" + reference_target(schema, source[, keyword="\$ref"]) + +Return the canonical target node bound to a reference keyword at `source`. +Return `nothing` when the compiled graph has no such binding. This lookup does +not copy the graph's reference table. +""" +function reference_target( + schema::CompiledSchema, + source::Resources.NodeId, + keyword::AbstractString = "\$ref", +) + canonical = Resources.canonical(schema.registry, source) + return get( + getfield(schema, :references), + (canonical, String(keyword)), + nothing, + ) +end + +function reference_target( + schemas::CompiledSchemas, + source::Resources.NodeId, + keyword::AbstractString = "\$ref", +) + return reference_target(getfield(schemas, :template), source, keyword) +end + +function _directory_resource(parent_dir::AbstractString) + path = abspath(expanduser(parent_dir)) + endswith(path, Base.Filesystem.path_separator) || + (path *= Base.Filesystem.path_separator) + return Resources.ResourceId(URIs.URI(; scheme = "file", path)) +end + +function _resource_id(base_uri, parent_dir) + if base_uri !== nothing + return base_uri isa Resources.ResourceId ? base_uri : + Resources.ResourceId(base_uri) + end + parent_dir === nothing && + return Resources.ResourceId("urn:jsonschema:anonymous") + return _directory_resource(parent_dir) +end + +function _source_child(source::Resources.NodeId, tokens) + pointer = source.pointer + for token in tokens + pointer = pointer / string(token) + end + return Resources.NodeId(source.resource, pointer) +end + +function _check_source!( + value, + count::Base.RefValue{Int}, + active::IdDict{Any,Nothing}, + max_nodes::Int, + max_depth::Int, + depth::Int = 0, +) + depth <= max_depth || throw( + ArgumentError("the JSON value exceeds the depth limit of $max_depth"), + ) + count[] += 1 + count[] <= max_nodes || + throw(ArgumentError("the JSON value exceeds the $max_nodes-node limit")) + (value isa AbstractDict || value isa AbstractVector) || return + haskey(active, value) && + throw(ArgumentError("the JSON value contains a reference cycle")) + active[value] = nothing + try + if value isa AbstractDict + for (key, child) in value + key isa AbstractString || + throw(ArgumentError("JSON object keys must be strings")) + _check_source!( + child, + count, + active, + max_nodes, + max_depth, + depth + 1, + ) + end + else + for child in value + _check_source!( + child, + count, + active, + max_nodes, + max_depth, + depth + 1, + ) + end + end + finally + delete!(active, value) + end + return +end + +function _check_source!(compiler::Compiler, value) + count = Ref(compiler.values) + _check_source!( + value, + count, + IdDict{Any,Nothing}(), + compiler.max_nodes, + compiler.max_depth, + ) + compiler.values = count[] + return +end + +function _node_child(node::Resources.NodeId, tokens) + pointer = node.pointer + for token in tokens + pointer = pointer / string(token) + end + return Resources.NodeId(node.resource, pointer) +end + +function _anchor_name(fragment::Resources.Fragment) + fragment isa Resources.RootFragment && return nothing + fragment isa Resources.AnchorFragment && return fragment.name + return throw( + ArgumentError("an identifier cannot use a JSON Pointer fragment"), + ) +end + +function _declared_identifier(schema::AbstractDict, schema_dialect::Dialect) + identifier = get(schema, schema_dialect.id_keyword, nothing) + identifier === nothing && return nothing + identifier isa AbstractString || return throw( + ArgumentError("$(schema_dialect.id_keyword) must be a string"), + ) + return String(identifier) +end + +_declared_identifier(::Bool, ::Dialect) = nothing + +function _root_identity( + schema, + retrieval::Resources.ResourceId, + schema_dialect::Dialect, +) + if schema isa AbstractDict && + !schema_dialect.ref_siblings && + haskey(schema, "\$ref") + return (retrieval, nothing) + end + identifier = _declared_identifier(schema, schema_dialect) + identifier === nothing && return (retrieval, nothing) + reference = Resources.Reference(retrieval, identifier) + anchor = _anchor_name(reference.fragment) + if anchor !== nothing && schema_dialect.name in (:draft201909, :draft202012) + throw( + ArgumentError( + "$(schema_dialect.id_keyword) cannot contain a non-empty fragment", + ), + ) + end + return (reference.resource, anchor) +end + +const CORE_VOCABULARIES = Set([ + "https://json-schema.org/draft/2019-09/vocab/core", + "https://json-schema.org/draft/2020-12/vocab/core", +]) +const APPLICATOR_VOCABULARIES = Set([ + "https://json-schema.org/draft/2019-09/vocab/applicator", + "https://json-schema.org/draft/2020-12/vocab/applicator", +]) +const VALIDATION_VOCABULARIES = Set([ + "https://json-schema.org/draft/2019-09/vocab/validation", + "https://json-schema.org/draft/2020-12/vocab/validation", +]) +const UNEVALUATED_VOCABULARIES = + Set(["https://json-schema.org/draft/2020-12/vocab/unevaluated"]) +const SUPPORTED_VOCABULARIES = union( + CORE_VOCABULARIES, + APPLICATOR_VOCABULARIES, + VALIDATION_VOCABULARIES, + UNEVALUATED_VOCABULARIES, + Set([ + "https://json-schema.org/draft/2019-09/vocab/meta-data", + "https://json-schema.org/draft/2019-09/vocab/format", + "https://json-schema.org/draft/2019-09/vocab/content", + "https://json-schema.org/draft/2020-12/vocab/meta-data", + "https://json-schema.org/draft/2020-12/vocab/format-annotation", + "https://json-schema.org/draft/2020-12/vocab/content", + ]), +) + +function _custom_dialect!( + compiler::Compiler, + uri::AbstractString, + default::Dialect, +) + normalized = _normalized_dialect_uri(uri) + cached = get(compiler.dialect_aliases, normalized, nothing) + cached === nothing || return cached + id = Resources.ResourceId(normalized) + retrieved = Resources.retrieve(compiler.retriever, id) + meta = try + JSON.parse(String(copy(retrieved.bytes))) + catch err + throw(UnsupportedDialectError("$uri ($(sprint(showerror, err)))")) + end + base = dialect(meta; default) + vocabularies = get(meta, "\$vocabulary", nothing) + applicator = base.applicator + validation = base.validation + unevaluated = base.unevaluated + if vocabularies isa AbstractDict + for (vocabulary, required) in vocabularies + vocabulary isa AbstractString || throw( + UnsupportedDialectError( + "$uri has a non-string vocabulary identifier", + ), + ) + required isa Bool || throw( + UnsupportedDialectError( + "$uri has a non-boolean vocabulary requirement", + ), + ) + if required && !(String(vocabulary) in SUPPORTED_VOCABULARIES) + throw( + UnsupportedDialectError( + "$uri requires unknown vocabulary $vocabulary", + ), + ) + end + end + any( + vocabulary -> String(vocabulary) in CORE_VOCABULARIES, + keys(vocabularies), + ) || throw( + UnsupportedDialectError( + "$uri does not declare the core vocabulary", + ), + ) + all( + vocabulary -> vocabularies[vocabulary] === true, + filter( + vocabulary -> String(vocabulary) in CORE_VOCABULARIES, + collect(keys(vocabularies)), + ), + ) || throw( + UnsupportedDialectError( + "$uri declares the core vocabulary as optional", + ), + ) + applicator = any( + vocabulary -> String(vocabulary) in APPLICATOR_VOCABULARIES, + keys(vocabularies), + ) + validation = any( + vocabulary -> String(vocabulary) in VALIDATION_VOCABULARIES, + keys(vocabularies), + ) + unevaluated = if base.name == :draft201909 + applicator + else + any( + vocabulary -> + String(vocabulary) in UNEVALUATED_VOCABULARIES, + keys(vocabularies), + ) + end + end + custom = Dialect( + base.name, + normalized, + base.id_keyword, + base.ref_siblings, + base.modern_items, + unevaluated, + base.dynamic_refs, + base.recursive_refs, + applicator, + validation, + ) + compiler.dialect_aliases[normalized] = custom + return custom +end + +function _schema_dialect!( + compiler::Compiler, + schema, + default::Dialect, + resource_root::Bool, +) + resource_root || return default + schema isa AbstractDict || return default + declared = get(schema, "\$schema", nothing) + declared === nothing && return default + declared isa AbstractString || + throw(UnsupportedDialectError(repr(declared))) + try + return dialect(declared) + catch err + err isa UnsupportedDialectError || rethrow() + return _custom_dialect!(compiler, declared, default) + end +end + +function _scan_dialect!( + compiler::Compiler, + schema, + default::Dialect, + resource_root::Bool, +) + resource_root && return _schema_dialect!(compiler, schema, default, true) + schema isa AbstractDict || return default + haskey(schema, "\$schema") || return default + candidate = _schema_dialect!(compiler, schema, default, true) + return _declared_identifier(schema, candidate) === nothing ? default : + candidate +end + +function _schema_map_children!(children, schema, keyword::String) + value = get(schema, keyword, nothing) + value isa AbstractDict || return + for (name, child) in value + (child isa AbstractDict || child isa Bool) || continue + push!(children, ((keyword, String(name)), child)) + end + return +end + +function _schema_array_children!(children, schema, keyword::String) + value = get(schema, keyword, nothing) + value isa AbstractVector || return + for (index, child) in enumerate(value) + (child isa AbstractDict || child isa Bool) || continue + push!(children, ((keyword, string(index - 1)), child)) + end + return +end + +function _schema_child!(children, schema, keyword::String) + value = get(schema, keyword, nothing) + (value isa AbstractDict || value isa Bool) || return + push!(children, ((keyword,), value)) + return +end + +function _schema_children(schema::AbstractDict, schema_dialect::Dialect) + children = Tuple{Tuple,Any}[] + for keyword in ("properties", "patternProperties") + keyword_applies(schema_dialect, keyword) && + _schema_map_children!(children, schema, keyword) + end + definitions = + schema_dialect.name in (:draft4, :draft6, :draft7) ? "definitions" : + "\$defs" + keyword_applies(schema_dialect, definitions) && + _schema_map_children!(children, schema, definitions) + keyword_applies(schema_dialect, "dependentSchemas") && + _schema_map_children!(children, schema, "dependentSchemas") + for keyword in ("allOf", "anyOf", "oneOf") + keyword_applies(schema_dialect, keyword) && + _schema_array_children!(children, schema, keyword) + end + keyword_applies(schema_dialect, "prefixItems") && + _schema_array_children!(children, schema, "prefixItems") + for keyword in ("not", "additionalProperties") + keyword_applies(schema_dialect, keyword) && + _schema_child!(children, schema, keyword) + end + keyword_applies(schema_dialect, "additionalItems") && + _schema_child!(children, schema, "additionalItems") + if keyword_applies(schema_dialect, "contains") + _schema_child!(children, schema, "contains") + end + if keyword_applies(schema_dialect, "propertyNames") + _schema_child!(children, schema, "propertyNames") + end + if keyword_applies(schema_dialect, "if") + for keyword in ("if", "then", "else") + _schema_child!(children, schema, keyword) + end + end + for keyword in + ("unevaluatedItems", "unevaluatedProperties", "contentSchema") + if keyword_applies(schema_dialect, keyword) + _schema_child!(children, schema, keyword) + end + end + items = get(schema, "items", nothing) + if items isa AbstractVector && + keyword_applies(schema_dialect, "items") && + !schema_dialect.modern_items + for (index, child) in enumerate(items) + (child isa AbstractDict || child isa Bool) || continue + push!(children, (("items", string(index - 1)), child)) + end + elseif keyword_applies(schema_dialect, "items") && + (items isa AbstractDict || items isa Bool) + push!(children, (("items",), items)) + end + dependencies = get(schema, "dependencies", nothing) + if dependencies isa AbstractDict && + keyword_applies(schema_dialect, "dependencies") + for (name, child) in dependencies + (child isa AbstractDict || child isa Bool) || continue + push!(children, (("dependencies", String(name)), child)) + end + end + return children +end + +_schema_children(::Bool, ::Dialect) = Tuple{Tuple,Any}[] + +function _register_anchor!( + compiler::Compiler, + node::Resources.NodeId, + name, + keyword::String; + dialect::Dialect, + dynamic::Bool = false, +) + name === nothing && return + name isa AbstractString || + throw(CompilationError(node, "$keyword must be a string")) + pattern = + dialect.name == :draft202012 ? r"^[A-Za-z_][-A-Za-z0-9._]*$" : + r"^[A-Za-z][-A-Za-z0-9._:]*$" + occursin(pattern, name) || + throw(CompilationError(node, "$keyword has an invalid anchor name")) + try + Resources.register_anchor!( + compiler.registry, + node.resource, + name, + node.pointer; + dynamic, + ) + catch err + err isa CompilationError && rethrow() + throw(CompilationError(node, sprint(showerror, err))) + end + return +end + +function _record_references!(compiler::Compiler, schema, node, schema_dialect) + schema isa AbstractDict || return + keywords = + schema_dialect.name == :draft202012 ? ("\$ref", "\$dynamicRef") : + schema_dialect.name == :draft201909 ? ("\$ref", "\$recursiveRef") : + ("\$ref",) + for keyword in keywords + reference = get(schema, keyword, nothing) + reference === nothing && continue + reference isa AbstractString || + throw(CompilationError(node, "$keyword must be a string")) + push!( + compiler.pending, + PendingReference( + node.resource, + keyword, + String(reference), + schema_dialect, + node, + ), + ) + end + return +end + +function _register_nested_resource!( + compiler::Compiler, + schema, + raw_node::Resources.NodeId, + source::Resources.NodeId, + schema_dialect::Dialect, + identifier::String, +) + reference = Resources.Reference(raw_node.resource, identifier) + anchor = _anchor_name(reference.fragment) + if anchor !== nothing && schema_dialect.name in (:draft201909, :draft202012) + throw( + CompilationError( + raw_node, + "$(schema_dialect.id_keyword) cannot contain a non-empty fragment", + ), + ) + end + if reference.resource == raw_node.resource && anchor !== nothing + _register_anchor!( + compiler, + raw_node, + anchor, + schema_dialect.id_keyword; + dialect = schema_dialect, + ) + return (raw_node, false) + end + if reference.resource == raw_node.resource && + anchor === nothing && + isempty(raw_node.pointer) + return (raw_node, true) + end + registered = Resources.resource(compiler.registry, raw_node.resource) + nested = Resources.Resource( + reference.resource, + schema; + retrieval = registered.retrieval, + source, + media_type = registered.media_type, + ) + try + length(compiler.registry.resources) < compiler.max_resources || throw( + Resources.RetrievalError( + reference.resource, + "the resource limit was reached", + ), + ) + Resources.register!(compiler.registry, nested; alias_retrieval = false) + Resources.register_boundary!( + compiler.registry, + raw_node, + Resources.NodeId(reference.resource, Resources.JSONPointer()), + ) + catch err + throw(CompilationError(raw_node, sprint(showerror, err))) + end + node = Resources.NodeId(reference.resource, Resources.JSONPointer()) + anchor === nothing || _register_anchor!( + compiler, + node, + anchor, + schema_dialect.id_keyword; + dialect = schema_dialect, + ) + return (node, true) +end + +_is_schema_value(value) = value isa AbstractDict || value isa Bool +_is_json_number(value) = value isa Real && !(value isa Bool) && isfinite(value) +function _is_nonnegative_integer(value) + return _is_json_number(value) && isinteger(value) && value >= 0 +end + +function _compile_regex!( + compiler::Compiler, + node::Resources.NodeId, + pattern::AbstractString, +) + normalized = String(pattern) + haskey(compiler.regexes, normalized) && return compiler.regexes[normalized] + regex = try + _ecma_regex(normalized) + catch err + throw( + CompilationError( + node, + "invalid ECMA-262 regular expression: $(sprint(showerror, err))", + ), + ) + end + compiler.regexes[normalized] = regex + return regex +end + +function _require_schema(node, keyword, value) + _is_schema_value(value) || throw( + CompilationError( + node, + "$keyword must contain an object or boolean schema", + ), + ) + return +end + +function _require_schema_map(node, keyword, value) + value isa AbstractDict || + throw(CompilationError(node, "$keyword must be an object")) + for (name, child) in value + name isa AbstractString || + throw(CompilationError(node, "$keyword keys must be strings")) + _require_schema(node, "$keyword[$(repr(name))]", child) + end + return +end + +function _require_schema_array(node, keyword, value) + value isa AbstractVector || + throw(CompilationError(node, "$keyword must be an array")) + isempty(value) && + throw(CompilationError(node, "$keyword must not be empty")) + for child in value + _require_schema(node, keyword, child) + end + return +end + +function _require_string_array(node, keyword, value; nonempty::Bool = false) + value isa AbstractVector || + throw(CompilationError(node, "$keyword must be an array")) + nonempty && + isempty(value) && + throw(CompilationError(node, "$keyword must not be empty")) + all(item -> item isa AbstractString, value) || + throw(CompilationError(node, "$keyword entries must be strings")) + length(Set(String.(value))) == length(value) || + throw(CompilationError(node, "$keyword entries must be unique")) + return +end + +function _validate_schema_keywords!( + compiler::Compiler, + schema::Bool, + node::Resources.NodeId, + schema_dialect::Dialect, +) + return +end + +function _validate_schema_keywords!( + compiler::Compiler, + schema::AbstractDict, + node::Resources.NodeId, + schema_dialect::Dialect, +) + for keyword in ("\$ref", "\$dynamicRef", "\$recursiveRef") + keyword_applies(schema_dialect, keyword) || continue + haskey(schema, keyword) || continue + schema[keyword] isa AbstractString || + throw(CompilationError(node, "$keyword must be a string")) + end + for keyword in ( + "properties", + "patternProperties", + "definitions", + "\$defs", + "dependentSchemas", + ) + keyword_applies(schema_dialect, keyword) || continue + haskey(schema, keyword) || continue + _require_schema_map(node, keyword, schema[keyword]) + end + patterns = get(schema, "patternProperties", nothing) + if keyword_applies(schema_dialect, "patternProperties") && + patterns isa AbstractDict + for pattern in keys(patterns) + _compile_regex!(compiler, node, String(pattern)) + end + end + for keyword in ("allOf", "anyOf", "oneOf", "prefixItems") + keyword_applies(schema_dialect, keyword) || continue + haskey(schema, keyword) || continue + _require_schema_array(node, keyword, schema[keyword]) + end + for keyword in ( + "not", + "additionalProperties", + "additionalItems", + "contains", + "propertyNames", + "if", + "then", + "else", + "unevaluatedItems", + "unevaluatedProperties", + "contentSchema", + ) + keyword_applies(schema_dialect, keyword) || continue + haskey(schema, keyword) || continue + _require_schema(node, keyword, schema[keyword]) + end + if keyword_applies(schema_dialect, "items") && haskey(schema, "items") + items = schema["items"] + if schema_dialect.modern_items + _require_schema(node, "items", items) + elseif items isa AbstractVector + isempty(items) || + foreach(child -> _require_schema(node, "items", child), items) + else + _require_schema(node, "items", items) + end + end + if keyword_applies(schema_dialect, "dependencies") && + haskey(schema, "dependencies") + dependencies = schema["dependencies"] + dependencies isa AbstractDict || + throw(CompilationError(node, "dependencies must be an object")) + for value in values(dependencies) + if value isa AbstractVector + _require_string_array(node, "dependencies", value) + else + _require_schema(node, "dependencies", value) + end + end + end + if keyword_applies(schema_dialect, "dependentRequired") && + haskey(schema, "dependentRequired") + dependencies = schema["dependentRequired"] + dependencies isa AbstractDict || + throw(CompilationError(node, "dependentRequired must be an object")) + for value in values(dependencies) + _require_string_array(node, "dependentRequired", value) + end + end + if keyword_applies(schema_dialect, "type") && haskey(schema, "type") + types = schema["type"] + allowed = Set([ + "null", + "boolean", + "object", + "array", + "number", + "string", + "integer", + ]) + if types isa AbstractString + String(types) in allowed || + throw(CompilationError(node, "type is not a JSON type")) + else + _require_string_array(node, "type", types; nonempty = true) + all(type -> String(type) in allowed, types) || throw( + CompilationError(node, "type contains an unknown JSON type"), + ) + end + end + if keyword_applies(schema_dialect, "enum") && haskey(schema, "enum") + enum = schema["enum"] + enum isa AbstractVector || + throw(CompilationError(node, "enum must be an array")) + end + if haskey(schema, "multipleOf") && + keyword_applies(schema_dialect, "multipleOf") + multiple = schema["multipleOf"] + _is_json_number(multiple) && multiple > 0 || throw( + CompilationError(node, "multipleOf must be a positive number"), + ) + end + for keyword in ("maximum", "minimum") + haskey(schema, keyword) && keyword_applies(schema_dialect, keyword) || + continue + _is_json_number(schema[keyword]) || + throw(CompilationError(node, "$keyword must be a number")) + end + for keyword in ("exclusiveMaximum", "exclusiveMinimum") + haskey(schema, keyword) && keyword_applies(schema_dialect, keyword) || + continue + valid = + schema_dialect.name == :draft4 ? schema[keyword] isa Bool : + _is_json_number(schema[keyword]) + valid || throw( + CompilationError( + node, + "$keyword has the wrong type for the dialect", + ), + ) + end + for keyword in ( + "maxLength", + "minLength", + "maxItems", + "minItems", + "maxProperties", + "minProperties", + "minContains", + "maxContains", + ) + haskey(schema, keyword) && keyword_applies(schema_dialect, keyword) || + continue + _is_nonnegative_integer(schema[keyword]) || throw( + CompilationError(node, "$keyword must be a non-negative integer"), + ) + end + if haskey(schema, "uniqueItems") && + keyword_applies(schema_dialect, "uniqueItems") + schema["uniqueItems"] isa Bool || + throw(CompilationError(node, "uniqueItems must be a boolean")) + end + if haskey(schema, "required") && keyword_applies(schema_dialect, "required") + _require_string_array(node, "required", schema["required"]) + end + if haskey(schema, "pattern") && keyword_applies(schema_dialect, "pattern") + pattern = schema["pattern"] + pattern isa AbstractString || + throw(CompilationError(node, "pattern must be a string")) + _compile_regex!(compiler, node, pattern) + end + return +end + +function _scan!( + compiler::Compiler, + schema, + node::Resources.NodeId, + source::Resources.NodeId, + default_dialect::Dialect; + resource_root::Bool = false, + identifier_applied::Bool = false, + depth::Int = 0, +) + requested = node + node = Resources.canonical(compiler.registry, node) + if haskey(compiler.dialects, node) + compiler.evaluation_nodes[requested] = compiler.evaluation_nodes[node] + return node + end + depth <= compiler.max_depth || throw( + CompilationError( + node, + "the schema exceeds the depth limit of $(compiler.max_depth)", + ), + ) + compiler.nodes += 1 + compiler.nodes <= compiler.max_nodes || throw( + CompilationError( + node, + "the schema exceeds the $(compiler.max_nodes)-node limit", + ), + ) + schema_dialect = try + _scan_dialect!(compiler, schema, default_dialect, resource_root) + catch err + throw(CompilationError(node, sprint(showerror, err))) + end + current = node + became_root = resource_root + reference_only = + schema isa AbstractDict && + !schema_dialect.ref_siblings && + haskey(schema, "\$ref") + if !identifier_applied && !reference_only + identifier = try + _declared_identifier(schema, schema_dialect) + catch err + throw(CompilationError(node, sprint(showerror, err))) + end + if identifier !== nothing + current, became_root = _register_nested_resource!( + compiler, + schema, + node, + source, + schema_dialect, + identifier, + ) + if became_root + schema_dialect = try + _schema_dialect!(compiler, schema, schema_dialect, true) + catch err + throw(CompilationError(current, sprint(showerror, err))) + end + end + end + end + compiled_node = + CompiledNode(compiler.nodes, current, schema, schema_dialect) + if schema isa AbstractDict && schema_dialect.unevaluated + compiler.uses_annotations |= + haskey(schema, "unevaluatedItems") || + haskey(schema, "unevaluatedProperties") + end + compiler.evaluation_nodes[requested] = compiled_node + compiler.evaluation_nodes[node] = compiled_node + compiler.evaluation_nodes[current] = compiled_node + compiler.dialects[current] = schema_dialect + if reference_only + _record_references!(compiler, schema, current, schema_dialect) + return current + end + _validate_schema_keywords!(compiler, schema, current, schema_dialect) + if schema isa AbstractDict + if schema_dialect.name in (:draft201909, :draft202012) + _register_anchor!( + compiler, + current, + get(schema, "\$anchor", nothing), + "\$anchor"; + dialect = schema_dialect, + ) + end + if schema_dialect.dynamic_refs + _register_anchor!( + compiler, + current, + get(schema, "\$dynamicAnchor", nothing), + "\$dynamicAnchor"; + dialect = schema_dialect, + dynamic = true, + ) + end + if schema_dialect.recursive_refs + recursive = get(schema, "\$recursiveAnchor", false) + recursive === true && + push!(compiler.recursive_anchors, current.resource) + (recursive isa Bool) || throw( + CompilationError( + current, + "\$recursiveAnchor must be a boolean", + ), + ) + end + end + _record_references!(compiler, schema, current, schema_dialect) + for (tokens, child) in _schema_children(schema, schema_dialect) + child_node = _node_child(current, tokens) + child_source = _source_child(source, tokens) + child_current = _scan!( + compiler, + child, + child_node, + child_source, + schema_dialect; + depth = depth + 1, + ) + transition = (compiled_node.index, tokens) + compiler.transitions[transition] = + compiler.evaluation_nodes[child_current] + end + return current +end + +function _compile_resource!( + compiler::Compiler, + schema, + retrieval::Resources.ResourceId, + default_dialect::Dialect; + media_type = nothing, +) + try + _check_source!(compiler, schema) + catch err + location = Resources.NodeId(retrieval, Resources.JSONPointer()) + throw(CompilationError(location, sprint(showerror, err))) + end + frozen = Resources.freeze(schema) + schema_dialect = try + _schema_dialect!(compiler, frozen, default_dialect, true) + catch err + location = Resources.NodeId(retrieval, Resources.JSONPointer()) + throw(CompilationError(location, sprint(showerror, err))) + end + canonical, anchor = try + _root_identity(frozen, retrieval, schema_dialect) + catch err + location = Resources.NodeId(retrieval, Resources.JSONPointer()) + throw(CompilationError(location, sprint(showerror, err))) + end + source = Resources.NodeId(retrieval, Resources.JSONPointer()) + resource = + Resources.Resource(canonical, frozen; retrieval, source, media_type) + try + Resources.register!(compiler.registry, resource) + catch err + throw(CompilationError(source, sprint(showerror, err))) + end + push!(compiler.loaded, retrieval) + push!(compiler.loaded, canonical) + root = Resources.NodeId(canonical, Resources.JSONPointer()) + anchor === nothing || _register_anchor!( + compiler, + root, + anchor, + schema_dialect.id_keyword; + dialect = schema_dialect, + ) + _scan!( + compiler, + frozen, + root, + source, + schema_dialect; + resource_root = true, + identifier_applied = true, + ) + return (root, frozen, schema_dialect) +end + +function _load_reference!( + compiler::Compiler, + target::Resources.ResourceId, + inherited::Dialect, +) + haskey(compiler.registry, target) && return + target in compiler.loading && return + length(compiler.registry.resources) < compiler.max_resources || throw( + Resources.RetrievalError(target, "the resource limit was reached"), + ) + push!(compiler.loading, target) + retrieved = try + Resources.retrieve(compiler.retriever, target) + catch + delete!(compiler.loading, target) + rethrow() + end + parsed = try + JSON.parse(String(copy(retrieved.bytes))) + catch err + delete!(compiler.loading, target) + throw( + Resources.RetrievalError( + target, + "invalid JSON: $(sprint(showerror, err))", + ), + ) + end + try + root, _, _ = _compile_resource!( + compiler, + parsed, + retrieved.id, + inherited; + media_type = retrieved.media_type, + ) + if target != retrieved.id && !haskey(compiler.registry, target) + Resources.register_alias!(compiler.registry, target, root.resource) + end + finally + delete!(compiler.loading, target) + end + return +end + +function _source_node(registry::Resources.AbstractRegistry, node::Resources.NodeId) + registered = Resources.resource(registry, node.resource) + pointer = registered.source.pointer + for token in node.pointer + pointer /= token + end + return Resources.NodeId(registered.source.resource, pointer) +end + +function _resolve_pending!(compiler::Compiler) + index = 1 + while index <= length(compiler.pending) + pending = compiler.pending[index] + reference = Resources.Reference(pending.base, pending.reference) + if !haskey(compiler.registry, reference.resource) + try + _load_reference!(compiler, reference.resource, pending.dialect) + catch err + throw( + CompilationError(pending.location, sprint(showerror, err)), + ) + end + end + resolved = try + Resources.resolve(compiler.registry, reference) + catch err + throw(CompilationError(pending.location, sprint(showerror, err))) + end + (resolved.value isa AbstractDict || resolved.value isa Bool) || throw( + CompilationError( + pending.location, + "$(pending.keyword) does not resolve to an object or boolean schema", + ), + ) + target = Resources.canonical(compiler.registry, resolved.id) + if !haskey(compiler.dialects, target) + target = _scan!( + compiler, + resolved.value, + target, + _source_node(compiler.registry, target), + pending.dialect, + ) + end + target = Resources.canonical(compiler.registry, target) + compiler.references[(pending.location, pending.keyword)] = target + index += 1 + end + return +end + +function CompiledSchema( + schema::Union{AbstractDict,Bool}; + dialect::Union{Dialect,Symbol,AbstractString} = DRAFT7, + base_uri = nothing, + parent_dir::Union{Nothing,AbstractString} = nothing, + dialect_aliases::AbstractDict = Dict{String,Dialect}(), + retriever::Resources.AbstractRetriever = Resources.DisabledRetriever(), + max_resources::Integer = 256, + max_nodes::Integer = 1_000_000, + max_depth::Integer = 512, +) + default_dialect = SchemaEngine.dialect(dialect) + retrieval = _resource_id(base_uri, parent_dir) + compiler = Compiler(retriever, max_resources, max_nodes, max_depth) + _register_dialect_aliases!(compiler, dialect_aliases) + root, frozen, schema_dialect = + _compile_resource!(compiler, schema, retrieval, default_dialect) + _resolve_pending!(compiler) + return CompiledSchema( + frozen, + schema_dialect, + Resources.freeze(compiler.registry), + root, + copy(compiler.dialects), + copy(compiler.dialect_aliases), + copy(compiler.evaluation_nodes), + copy(compiler.transitions), + compiler.uses_annotations, + copy(compiler.recursive_anchors), + copy(compiler.references), + copy(compiler.regexes), + retriever, + ) +end + +function _compiled_schema(compiler::Compiler, root::Resources.NodeId) + canonical = Resources.canonical(compiler.registry, root) + resource = Resources.resource(compiler.registry, canonical.resource) + data = Resources.resolve(resource.contents, canonical.pointer) + schema_dialect = get(compiler.dialects, canonical, DRAFT7) + return CompiledSchema( + data, + schema_dialect, + Resources.freeze(compiler.registry), + canonical, + copy(compiler.dialects), + copy(compiler.dialect_aliases), + copy(compiler.evaluation_nodes), + copy(compiler.transitions), + compiler.uses_annotations, + copy(compiler.recursive_anchors), + copy(compiler.references), + copy(compiler.regexes), + compiler.retriever, + ) +end + +function _root_dialect!(compiler::Compiler, value, fallback::Dialect) + value isa Dialect && return value + try + return dialect(value) + catch error + error isa UnsupportedDialectError || rethrow() + value isa AbstractString || rethrow() + return _custom_dialect!(compiler, value, fallback) + end +end + +""" + CompiledSchemas(resources, roots; options...) + +Compile several JSON Schema roots embedded in one or more registered JSON +resources. `roots` contains `Resources.NodeId` values. All roots are scanned +before references are resolved, so a reference can target a sibling schema by +its `\$id` or anchor without retrieving another document. `root_dialects` can +map each requested or canonical root to a `Dialect`, registered dialect symbol, +or dialect URI. Roots not in the map use `dialect`. `dialect_aliases` maps +application dialect URI strings to compatible built-in dialects without +retrieving a meta-schema. +""" +function CompiledSchemas( + resources::AbstractVector{<:Resources.Resource}, + roots::AbstractVector{<:Resources.NodeId}; + dialect::Union{Dialect,Symbol,AbstractString} = DRAFT7, + root_dialects::AbstractDict = Dict{Resources.NodeId,Dialect}(), + dialect_aliases::AbstractDict = Dict{String,Dialect}(), + retriever::Resources.AbstractRetriever = Resources.DisabledRetriever(), + max_resources::Integer = 256, + max_nodes::Integer = 1_000_000, + max_depth::Integer = 512, +) + isempty(resources) && + throw(ArgumentError("at least one resource is required")) + isempty(roots) && + throw(ArgumentError("at least one schema root is required")) + length(resources) <= max_resources || + throw(ArgumentError("initial resources exceed max_resources")) + default_dialect = SchemaEngine.dialect(dialect) + compiler = Compiler(retriever, max_resources, max_nodes, max_depth) + _register_dialect_aliases!(compiler, dialect_aliases) + for resource in resources + try + _check_source!(compiler, resource.contents) + Resources.register!(compiler.registry, resource) + push!(compiler.loaded, resource.id) + push!(compiler.loaded, resource.retrieval) + catch error + throw(CompilationError(resource.source, sprint(showerror, error))) + end + end + compiled_roots = Dict{Resources.NodeId,Resources.NodeId}() + for requested in roots + registered = try + Resources.resource(compiler.registry, requested.resource) + catch error + throw(CompilationError(requested, sprint(showerror, error))) + end + raw = Resources.NodeId(registered.id, requested.pointer) + value = try + Resources.resolve(registered.contents, requested.pointer) + catch error + throw(CompilationError(raw, sprint(showerror, error))) + end + (value isa AbstractDict || value isa Bool) || throw( + CompilationError( + raw, + "the selected value is not an object or boolean schema", + ), + ) + selected_dialect = get( + root_dialects, + requested, + get(root_dialects, raw, default_dialect), + ) + schema_dialect = try + _root_dialect!(compiler, selected_dialect, default_dialect) + catch error + throw(CompilationError(raw, sprint(showerror, error))) + end + root = _scan!( + compiler, + value, + raw, + _source_node(compiler.registry, raw), + schema_dialect; + resource_root = true, + ) + compiled_roots[requested] = root + compiled_roots[raw] = root + end + _resolve_pending!(compiler) + for (requested, root) in collect(compiled_roots) + compiled_roots[requested] = Resources.canonical(compiler.registry, root) + end + template = _compiled_schema(compiler, first(values(compiled_roots))) + return CompiledSchemas(template, compiled_roots) +end + +function CompiledSchemas( + resource::Resources.Resource, + pointers::AbstractVector{<:Resources.JSONPointer}; + kwargs..., +) + roots = Resources.NodeId[ + Resources.NodeId(resource.id, pointer) for pointer in pointers + ] + return CompiledSchemas([resource], roots; kwargs...) +end + +function select(schemas::CompiledSchemas, requested::Resources.NodeId) + template = getfield(schemas, :template) + roots = getfield(schemas, :roots) + root = get(roots, requested, nothing) + if root === nothing + canonical = Resources.canonical(template.registry, requested) + root = get(roots, canonical, nothing) + end + root === nothing && + throw(ArgumentError("the requested node is not a compiled schema root")) + resource = Resources.resource(template.registry, root.resource) + data = Resources.resolve(resource.contents, root.pointer) + schema_dialect = get(getfield(template, :dialects), root, template.dialect) + return CompiledSchema( + data, + schema_dialect, + template.registry, + root, + getfield(template, :dialects), + getfield(template, :dialect_aliases), + getfield(template, :evaluation_nodes), + getfield(template, :transitions), + template.uses_annotations, + getfield(template, :recursive_anchors), + getfield(template, :references), + getfield(template, :regexes), + template.retriever, + ) +end + +function select( + schemas::CompiledSchemas, + resource::Resources.ResourceId, + pointer::Resources.JSONPointer = Resources.JSONPointer(), +) + return select(schemas, Resources.NodeId(resource, pointer)) +end + +"""Return a compiled view of any schema node scanned in a schema graph.""" +function subschema(schemas::CompiledSchemas, requested::Resources.NodeId) + template = getfield(schemas, :template) + return subschema(template, requested) +end + +"""Return a compiled view of any schema node scanned in a compiled graph.""" +function subschema(template::CompiledSchema, requested::Resources.NodeId) + canonical = Resources.canonical(template.registry, requested) + node = get(getfield(template, :evaluation_nodes), canonical, nothing) + node === nothing && throw( + ArgumentError("the requested node is not a compiled schema location"), + ) + root = node.id + resource = Resources.resource(template.registry, root.resource) + data = Resources.resolve(resource.contents, root.pointer) + schema_dialect = get(getfield(template, :dialects), root, template.dialect) + return CompiledSchema( + data, + schema_dialect, + template.registry, + root, + getfield(template, :dialects), + getfield(template, :dialect_aliases), + getfield(template, :evaluation_nodes), + getfield(template, :transitions), + template.uses_annotations, + getfield(template, :recursive_anchors), + getfield(template, :references), + getfield(template, :regexes), + template.retriever, + ) +end + +function subschema( + schemas::CompiledSchemas, + resource::Resources.ResourceId, + pointer::Resources.JSONPointer = Resources.JSONPointer(), +) + return subschema(schemas, Resources.NodeId(resource, pointer)) +end + +function CompiledSchema( + resource::Resources.Resource, + pointer::Resources.JSONPointer = Resources.JSONPointer(); + dialect::Union{Dialect,Symbol,AbstractString} = DRAFT7, + dialect_aliases::AbstractDict = Dict{String,Dialect}(), + retriever::Resources.AbstractRetriever = Resources.DisabledRetriever(), + max_resources::Integer = 256, + max_nodes::Integer = 1_000_000, + max_depth::Integer = 512, +) + default_dialect = SchemaEngine.dialect(dialect) + compiler = Compiler(retriever, max_resources, max_nodes, max_depth) + _register_dialect_aliases!(compiler, dialect_aliases) + try + _check_source!(compiler, resource.contents) + Resources.register!(compiler.registry, resource) + catch err + location = Resources.NodeId(resource.id, pointer) + throw(CompilationError(location, sprint(showerror, err))) + end + raw_root = Resources.NodeId(resource.id, pointer) + data = try + Resources.resolve(resource.contents, pointer) + catch err + throw(CompilationError(raw_root, sprint(showerror, err))) + end + (data isa AbstractDict || data isa Bool) || throw( + CompilationError( + raw_root, + "the selected value is not an object or boolean schema", + ), + ) + document_root = Resources.NodeId(resource.id, Resources.JSONPointer()) + if resource.contents isa AbstractDict || resource.contents isa Bool + _scan!( + compiler, + resource.contents, + document_root, + _source_node(compiler.registry, document_root), + default_dialect; + resource_root = true, + ) + end + selected = get(compiler.evaluation_nodes, raw_root, nothing) + root = if selected === nothing + _scan!( + compiler, + data, + raw_root, + _source_node(compiler.registry, raw_root), + default_dialect; + resource_root = true, + ) + else + selected.id + end + _resolve_pending!(compiler) + root = Resources.canonical(compiler.registry, root) + schema_dialect = get(compiler.dialects, root, default_dialect) + return CompiledSchema( + data, + schema_dialect, + Resources.freeze(compiler.registry), + root, + copy(compiler.dialects), + copy(compiler.dialect_aliases), + copy(compiler.evaluation_nodes), + copy(compiler.transitions), + compiler.uses_annotations, + copy(compiler.recursive_anchors), + copy(compiler.references), + copy(compiler.regexes), + retriever, + ) +end + +function CompiledSchema(schema::AbstractString; kwargs...) + return CompiledSchema(JSON.parse(schema); kwargs...) +end + +spec(schema::CompiledSchema) = schema.data +Base.getindex(schema::CompiledSchema, key) = schema.data[key] +Base.haskey(schema::CompiledSchema, key) = haskey(schema.data, key) +Base.get(schema::CompiledSchema, key, default) = get(schema.data, key, default) +Base.keys(schema::CompiledSchema) = keys(schema.data) +JSON.lower(schema::CompiledSchema) = schema.data + +function Base.show(io::IO, schema::CompiledSchema) + return print(io, "A compiled JSONSchema ($(schema.dialect.name))") +end diff --git a/src/schema_engine/compiled_validation.jl b/src/schema_engine/compiled_validation.jl new file mode 100644 index 0000000..ba1e098 --- /dev/null +++ b/src/schema_engine/compiled_validation.jl @@ -0,0 +1,1219 @@ +# Copyright (c) 2026: fredo-dedup, quinnj, and contributors +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +mutable struct EvaluationResult + valid::Bool + issues::Union{Nothing,Vector{SingleIssue}} + properties::Union{Nothing,Set{String}} + items::Union{Nothing,BitSet} +end + +function EvaluationResult() + return EvaluationResult(true, nothing, nothing, nothing) +end + +mutable struct EvaluationPath + parent::Union{Nothing,EvaluationPath} + token::String + depth::Int +end + +EvaluationPath() = EvaluationPath(nothing, "", 0) +function EvaluationPath(parent::EvaluationPath, token::AbstractString) + return EvaluationPath(parent, String(token), parent.depth + 1) +end + +function _path_string(path::EvaluationPath) + path.depth == 0 && return "" + tokens = Vector{String}(undef, path.depth) + current = path + for index in path.depth:-1:1 + tokens[index] = current.token + current = current.parent::EvaluationPath + end + return string(Resources.JSONPointer(Tuple(tokens))) +end + +mutable struct EvaluationContext + schema::CompiledSchema + active::Set{Tuple{Int,EvaluationPath,Tuple{Vararg{Resources.ResourceId}}}} + regexes::Dict{String,Regex} + evaluations::Int + depth::Int + max_evaluations::Int + max_issues::Int + max_depth::Int + collect_all::Bool + annotations::Bool + tracks_cycles::Bool +end + +function EvaluationContext( + schema::CompiledSchema, + max_evaluations::Integer, + max_issues::Integer, + max_depth::Integer, + collect_all::Bool, +) + max_evaluations > 0 || + throw(ArgumentError("max_evaluations must be positive")) + max_issues > 0 || throw(ArgumentError("max_issues must be positive")) + max_depth > 0 || throw(ArgumentError("max_depth must be positive")) + return EvaluationContext( + schema, + Set{Tuple{Int,EvaluationPath,Tuple{Vararg{Resources.ResourceId}}}}(), + copy(getfield(schema, :regexes)), + 0, + 0, + Int(max_evaluations), + Int(max_issues), + Int(max_depth), + collect_all, + getfield(schema, :uses_annotations), + !isempty(getfield(schema, :references)), + ) +end + +struct EvaluationError <: Exception + schema::Resources.NodeId + instance::String + reason::String +end + +function Base.showerror(io::IO, err::EvaluationError) + pointer = string(err.schema.pointer) + location = + string(err.schema.resource) * (isempty(pointer) ? "" : "#" * pointer) + instance = + isempty(err.instance) ? "the instance root" : + "instance " * repr(err.instance) + return print( + io, + "cannot evaluate JSON Schema at ", + repr(location), + " for ", + instance, + ": ", + err.reason, + ) +end + +function _regex(context::EvaluationContext, pattern::AbstractString) + normalized = String(pattern) + return get!(context.regexes, normalized) do + return _ecma_regex(normalized) + end +end + +function _invalidate!( + result::EvaluationResult, + context::EvaluationContext, + issue::SingleIssue, +) + !context.collect_all && !result.valid && return result + if context.collect_all + issue_count = result.issues === nothing ? 0 : length(result.issues) + issue_count < context.max_issues || throw( + EvaluationError( + context.schema.root, + issue.path, + "the issue limit was reached", + ), + ) + end + result.valid = false + result.issues === nothing && (result.issues = SingleIssue[]) + push!(result.issues::Vector{SingleIssue}, issue) + return result +end + +function _merge_annotations!(result::EvaluationResult, child::EvaluationResult) + if child.properties !== nothing + if result.properties === nothing + result.properties = copy(child.properties) + else + union!(result.properties::Set{String}, child.properties) + end + end + if child.items !== nothing + if result.items === nothing + result.items = copy(child.items) + else + union!(result.items::BitSet, child.items) + end + end + return result +end + +function _mark_property!(result::EvaluationResult, name::String) + result.properties === nothing && (result.properties = Set{String}()) + push!(result.properties::Set{String}, name) + return result +end + +function _mark_item!(result::EvaluationResult, index::Int) + result.items === nothing && (result.items = BitSet()) + push!(result.items::BitSet, index) + return result +end + +function _absorb!( + result::EvaluationResult, + context::EvaluationContext, + child::EvaluationResult; + annotations::Bool = true, +) + if child.valid + annotations && context.annotations && _merge_annotations!(result, child) + else + result.valid = false + child_issues = something(child.issues, SingleIssue[]) + if context.collect_all + issue_count = result.issues === nothing ? 0 : length(result.issues) + issue_count + length(child_issues) <= context.max_issues || throw( + EvaluationError( + context.schema.root, + isempty(child_issues) ? "" : first(child_issues).path, + "the issue limit was reached", + ), + ) + if !isempty(child_issues) + result.issues === nothing && (result.issues = SingleIssue[]) + append!(result.issues::Vector{SingleIssue}, child_issues) + end + elseif result.issues === nothing && !isempty(child_issues) + result.issues = SingleIssue[first(child_issues)] + end + end + return result +end + +function _stopped(context::EvaluationContext, result::EvaluationResult) + return !context.collect_all && !result.valid +end + +function _compiled_node(schema::CompiledSchema, raw::Resources.NodeId) + nodes = getfield(schema, :evaluation_nodes) + found = get(nodes, raw, nothing) + found === nothing || return found + canonical = Resources.canonical(schema.registry, raw) + return get( + () -> throw( + EvaluationError( + canonical, + "", + "the location was not compiled as a schema", + ), + ), + nodes, + canonical, + ) +end + +function _canonical(schema::CompiledSchema, node::Resources.NodeId) + return _compiled_node(schema, node).id +end + +function _compiled_child(schema::CompiledSchema, node::Resources.NodeId, tokens) + parent = _compiled_node(schema, node) + key = (parent.index, tokens) + return get( + () -> throw( + EvaluationError( + parent.id, + "", + "the child location was not compiled as a schema", + ), + ), + getfield(schema, :transitions), + key, + ) +end + +function _property_path(path::EvaluationPath, property) + return EvaluationPath(path, string(property)) +end + +function _array_path(path::EvaluationPath, index::Integer) + return EvaluationPath(path, string(index - 1)) +end + +function _issue(x, path::EvaluationPath, keyword::String, value) + return SingleIssue(x, _path_string(path), keyword, value) +end + +function _json_type(x, type::AbstractString) + type == "null" && return x === nothing || x === missing + type == "boolean" && return x isa Bool + type == "object" && return x isa AbstractDict + type == "array" && return x isa AbstractVector + type == "string" && return x isa AbstractString + if type == "integer" + x isa Bool && return false + return x isa Integer || (x isa Real && isinteger(x)) + end + type == "number" && return x isa Real && !(x isa Bool) + return false +end + +function _type_valid(x, expected) + expected isa AbstractString && return _json_type(x, expected) + expected isa AbstractVector || return true + return any(type -> type isa AbstractString && _json_type(x, type), expected) +end + +function _multiple_of(x::Real, divisor::Real) + divisor == 0 && return false + ratio = x / divisor + isfinite(ratio) || return false + return isinteger(ratio) || + isapprox(ratio, round(ratio); rtol = 0, atol = eps(float(ratio)) * 4) +end + +function _simple_assertions!( + result::EvaluationResult, + context::EvaluationContext, + x, + schema::AbstractDict, + schema_dialect::Dialect, + path::EvaluationPath, +) + expected = get(schema, "type", nothing) + if expected !== nothing && !_type_valid(x, expected) + _invalidate!(result, context, _issue(x, path, "type", expected)) + end + enum = get(schema, "enum", nothing) + if enum isa AbstractVector && !any(value -> _isequal(x, value), enum) + _invalidate!(result, context, _issue(x, path, "enum", enum)) + end + if keyword_applies(schema_dialect, "const") && + haskey(schema, "const") && + !_isequal(x, schema["const"]) + _invalidate!(result, context, _issue(x, path, "const", schema["const"])) + end + if x isa Real && !(x isa Bool) + multiple = get(schema, "multipleOf", nothing) + if multiple isa Real && !_multiple_of(x, multiple) + _invalidate!( + result, + context, + _issue(x, path, "multipleOf", multiple), + ) + end + maximum = get(schema, "maximum", nothing) + if maximum isa Real && x > maximum + _invalidate!(result, context, _issue(x, path, "maximum", maximum)) + end + minimum = get(schema, "minimum", nothing) + if minimum isa Real && x < minimum + _invalidate!(result, context, _issue(x, path, "minimum", minimum)) + end + exclusive_maximum = get(schema, "exclusiveMaximum", nothing) + if schema_dialect.name != :draft4 && + exclusive_maximum isa Real && + !(exclusive_maximum isa Bool) && + x >= exclusive_maximum + _invalidate!( + result, + context, + _issue(x, path, "exclusiveMaximum", exclusive_maximum), + ) + elseif schema_dialect.name == :draft4 && + exclusive_maximum === true && + maximum isa Real && + x >= maximum + _invalidate!( + result, + context, + _issue(x, path, "exclusiveMaximum", exclusive_maximum), + ) + end + exclusive_minimum = get(schema, "exclusiveMinimum", nothing) + if schema_dialect.name != :draft4 && + exclusive_minimum isa Real && + !(exclusive_minimum isa Bool) && + x <= exclusive_minimum + _invalidate!( + result, + context, + _issue(x, path, "exclusiveMinimum", exclusive_minimum), + ) + elseif schema_dialect.name == :draft4 && + exclusive_minimum === true && + minimum isa Real && + x <= minimum + _invalidate!( + result, + context, + _issue(x, path, "exclusiveMinimum", exclusive_minimum), + ) + end + end + if x isa AbstractString + maximum = get(schema, "maxLength", nothing) + maximum isa Real && + isinteger(maximum) && + length(x) > maximum && + _invalidate!(result, context, _issue(x, path, "maxLength", maximum)) + minimum = get(schema, "minLength", nothing) + minimum isa Real && + isinteger(minimum) && + length(x) < minimum && + _invalidate!(result, context, _issue(x, path, "minLength", minimum)) + pattern = get(schema, "pattern", nothing) + pattern isa AbstractString && + !occursin(_regex(context, pattern), x) && + _invalidate!(result, context, _issue(x, path, "pattern", pattern)) + elseif x isa AbstractVector + maximum = get(schema, "maxItems", nothing) + maximum isa Real && + isinteger(maximum) && + length(x) > maximum && + _invalidate!(result, context, _issue(x, path, "maxItems", maximum)) + minimum = get(schema, "minItems", nothing) + minimum isa Real && + isinteger(minimum) && + length(x) < minimum && + _invalidate!(result, context, _issue(x, path, "minItems", minimum)) + if get(schema, "uniqueItems", false) === true + for left in eachindex(x), right in firstindex(x):(left-1) + if _isequal(x[left], x[right]) + _invalidate!( + result, + context, + _issue(x, path, "uniqueItems", true), + ) + break + end + end + end + elseif x isa AbstractDict + maximum = get(schema, "maxProperties", nothing) + maximum isa Real && + isinteger(maximum) && + length(x) > maximum && + _invalidate!( + result, + context, + _issue(x, path, "maxProperties", maximum), + ) + minimum = get(schema, "minProperties", nothing) + minimum isa Real && + isinteger(minimum) && + length(x) < minimum && + _invalidate!( + result, + context, + _issue(x, path, "minProperties", minimum), + ) + required = get(schema, "required", nothing) + if required isa AbstractVector + all(name -> haskey(x, name), required) || _invalidate!( + result, + context, + _issue(x, path, "required", required), + ) + end + dependent = get(schema, "dependentRequired", nothing) + keyword_applies(schema_dialect, "dependentRequired") && + dependent isa AbstractDict && + _dependent_required!( + result, + context, + x, + dependent, + path, + "dependentRequired", + ) + dependencies = get(schema, "dependencies", nothing) + keyword_applies(schema_dialect, "dependencies") && + dependencies isa AbstractDict && + _dependent_required!( + result, + context, + x, + dependencies, + path, + "dependencies", + ) + end + return result +end + +_simple_assertions!(result, context, x, ::Bool, schema_dialect, path) = result + +function _dependent_required!(result, context, x, dependencies, path, keyword) + for (property, required) in dependencies + haskey(x, property) || continue + required isa AbstractVector || continue + all(name -> haskey(x, name), required) || _invalidate!( + result, + context, + _issue(x, path, keyword, dependencies), + ) + end + return result +end + +function _reference_target( + context::EvaluationContext, + node::Resources.NodeId, + keyword::String, + reference_text::AbstractString, + dynamic_scope::Vector{Resources.ResourceId}; + dynamic::Bool = false, + recursive::Bool = false, +) + canonical_node = _canonical(context.schema, node) + reference = + dynamic ? Resources.Reference(canonical_node.resource, reference_text) : + nothing + target = get( + getfield(context.schema, :references), + (canonical_node, keyword), + nothing, + ) + if target === nothing + target = try + fallback = something( + reference, + Resources.Reference(canonical_node.resource, reference_text), + ) + resolved = Resources.resolve(context.schema.registry, fallback) + _canonical(context.schema, resolved.id) + catch err + throw(EvaluationError(canonical_node, "", sprint(showerror, err))) + end + end + if dynamic && reference.fragment isa Resources.AnchorFragment + name = reference.fragment.name + if Resources.dynamic_anchor( + context.schema.registry, + target.resource, + name, + ) !== nothing + for resource in dynamic_scope + candidate = Resources.dynamic_anchor( + context.schema.registry, + resource, + name, + ) + candidate === nothing || + return _canonical(context.schema, candidate) + end + end + elseif recursive && + target.resource in getfield(context.schema, :recursive_anchors) + for resource in dynamic_scope + resource in getfield(context.schema, :recursive_anchors) || continue + return Resources.NodeId(resource, Resources.JSONPointer()) + end + end + return target +end + +function _follow_reference( + context::EvaluationContext, + node::Resources.NodeId, + keyword::String, + reference_text::AbstractString, + x, + path::EvaluationPath, + dynamic_scope::Vector{Resources.ResourceId}; + dynamic::Bool = false, + recursive::Bool = false, +) + target = _reference_target( + context, + node, + keyword, + reference_text, + dynamic_scope; + dynamic, + recursive, + ) + next_scope = dynamic_scope + if isempty(dynamic_scope) || last(dynamic_scope) != target.resource + next_scope = copy(dynamic_scope) + push!(next_scope, target.resource) + end + return _evaluate_compiled_node( + context, + _compiled_node(context.schema, target), + x, + path, + next_scope, + ) +end + +function _references!( + result::EvaluationResult, + context::EvaluationContext, + node::Resources.NodeId, + x, + schema::AbstractDict, + schema_dialect::Dialect, + path::EvaluationPath, + dynamic_scope, +) + reference = get(schema, "\$ref", nothing) + if reference isa AbstractString + child = _follow_reference( + context, + node, + "\$ref", + reference, + x, + path, + dynamic_scope, + ) + _absorb!(result, context, child) + (_stopped(context, result) || !schema_dialect.ref_siblings) && + return false + end + if schema_dialect.dynamic_refs + reference = get(schema, "\$dynamicRef", nothing) + if reference isa AbstractString + child = _follow_reference( + context, + node, + "\$dynamicRef", + reference, + x, + path, + dynamic_scope; + dynamic = true, + ) + _absorb!(result, context, child) + _stopped(context, result) && return false + end + elseif schema_dialect.recursive_refs + reference = get(schema, "\$recursiveRef", nothing) + if reference isa AbstractString + child = _follow_reference( + context, + node, + "\$recursiveRef", + reference, + x, + path, + dynamic_scope; + recursive = true, + ) + _absorb!(result, context, child) + _stopped(context, result) && return false + end + end + return true +end + +function _child_result(context, node, tokens, x, path, dynamic_scope) + child = _compiled_child(context.schema, node, tokens) + return _evaluate_compiled_node(context, child, x, path, dynamic_scope) +end + +function _combinators!( + result, + context, + node, + x, + schema::AbstractDict, + schema_dialect::Dialect, + path, + dynamic_scope, +) + for keyword in ("allOf",) + schemas = get(schema, keyword, nothing) + schemas isa AbstractVector || continue + for index in eachindex(schemas) + child = _child_result( + context, + node, + (keyword, string(index - 1)), + x, + path, + dynamic_scope, + ) + _absorb!(result, context, child) + _stopped(context, result) && return result + end + end + for keyword in ("anyOf", "oneOf") + schemas = get(schema, keyword, nothing) + schemas isa AbstractVector || continue + valid = EvaluationResult[] + for index in eachindex(schemas) + child = _child_result( + context, + node, + (keyword, string(index - 1)), + x, + path, + dynamic_scope, + ) + child.valid && push!(valid, child) + end + expected = keyword == "anyOf" ? !isempty(valid) : length(valid) == 1 + if expected + for child in valid + _absorb!(result, context, child) + end + else + _invalidate!(result, context, _issue(x, path, keyword, schemas)) + _stopped(context, result) && return result + end + end + negated = get(schema, "not", nothing) + if negated isa AbstractDict || negated isa Bool + child = _child_result(context, node, ("not",), x, path, dynamic_scope) + if child.valid + _invalidate!(result, context, _issue(x, path, "not", negated)) + _stopped(context, result) && return result + end + end + condition = get(schema, "if", nothing) + if keyword_applies(schema_dialect, "if") && + (condition isa AbstractDict || condition isa Bool) + child = _child_result(context, node, ("if",), x, path, dynamic_scope) + child.valid && _absorb!(result, context, child) + branch = child.valid ? "then" : "else" + selected = get(schema, branch, nothing) + if selected isa AbstractDict || selected isa Bool + branch_result = + _child_result(context, node, (branch,), x, path, dynamic_scope) + _absorb!(result, context, branch_result) + _stopped(context, result) && return result + end + end + return result +end + +function _object_applicators!( + result, + context, + node, + x::AbstractDict, + schema::AbstractDict, + schema_dialect::Dialect, + path, + dynamic_scope, +) + additional = get(schema, "additionalProperties", nothing) + tracks_coverage = additional isa AbstractDict || additional isa Bool + covered = tracks_coverage ? Set{String}() : nothing + properties = get(schema, "properties", nothing) + if properties isa AbstractDict + for (name, subschema) in properties + haskey(x, name) || continue + (subschema isa AbstractDict || subschema isa Bool) || continue + child = _child_result( + context, + node, + ("properties", String(name)), + x[name], + _property_path(path, name), + dynamic_scope, + ) + _absorb!(result, context, child; annotations = false) + _stopped(context, result) && return result + tracks_coverage && push!(covered::Set{String}, String(name)) + context.annotations && _mark_property!(result, String(name)) + end + end + patterns = get(schema, "patternProperties", nothing) + if patterns isa AbstractDict + for (pattern, subschema) in patterns + (subschema isa AbstractDict || subschema isa Bool) || continue + regex = _regex(context, pattern) + for (name, value) in x + occursin(regex, string(name)) || continue + child = _child_result( + context, + node, + ("patternProperties", String(pattern)), + value, + _property_path(path, name), + dynamic_scope, + ) + _absorb!(result, context, child; annotations = false) + _stopped(context, result) && return result + tracks_coverage && push!(covered::Set{String}, String(name)) + context.annotations && _mark_property!(result, String(name)) + end + end + end + if additional isa AbstractDict || additional isa Bool + for (name, value) in x + string(name) in (covered::Set{String}) && continue + child = _child_result( + context, + node, + ("additionalProperties",), + value, + _property_path(path, name), + dynamic_scope, + ) + _absorb!(result, context, child; annotations = false) + _stopped(context, result) && return result + context.annotations && _mark_property!(result, String(name)) + end + end + names = get(schema, "propertyNames", nothing) + if keyword_applies(schema_dialect, "propertyNames") && + (names isa AbstractDict || names isa Bool) + for name in keys(x) + child = _child_result( + context, + node, + ("propertyNames",), + string(name), + _property_path(path, name), + dynamic_scope, + ) + _absorb!(result, context, child; annotations = false) + _stopped(context, result) && return result + end + end + for keyword in ("dependencies", "dependentSchemas") + keyword_applies(schema_dialect, keyword) || continue + dependencies = get(schema, keyword, nothing) + dependencies isa AbstractDict || continue + for (name, subschema) in dependencies + haskey(x, name) || continue + (subschema isa AbstractDict || subschema isa Bool) || continue + child = _child_result( + context, + node, + (keyword, String(name)), + x, + path, + dynamic_scope, + ) + _absorb!(result, context, child) + _stopped(context, result) && return result + end + end + return result +end + +function _object_applicators!( + result, + context, + node, + x, + schema, + schema_dialect, + path, + dynamic_scope, +) + return result +end + +function _array_item!( + result, + context, + node, + tokens, + x, + index, + path, + dynamic_scope, +) + child = _child_result( + context, + node, + tokens, + x[index], + _array_path(path, index), + dynamic_scope, + ) + _absorb!(result, context, child; annotations = false) + context.annotations && _mark_item!(result, index) + return result +end + +function _array_applicators!( + result, + context, + node, + x::AbstractVector, + schema::AbstractDict, + schema_dialect::Dialect, + path, + dynamic_scope, +) + prefix = get(schema, "prefixItems", nothing) + prefix_count = 0 + if schema_dialect.modern_items && prefix isa AbstractVector + prefix_count = min(length(prefix), length(x)) + for index in 1:prefix_count + _array_item!( + result, + context, + node, + ("prefixItems", string(index - 1)), + x, + index, + path, + dynamic_scope, + ) + _stopped(context, result) && return result + end + end + items = get(schema, "items", nothing) + if items isa AbstractVector && !schema_dialect.modern_items + tuple_count = min(length(items), length(x)) + for index in 1:tuple_count + _array_item!( + result, + context, + node, + ("items", string(index - 1)), + x, + index, + path, + dynamic_scope, + ) + _stopped(context, result) && return result + end + additional = get(schema, "additionalItems", nothing) + if additional isa AbstractDict || additional isa Bool + for index in (tuple_count+1):length(x) + _array_item!( + result, + context, + node, + ("additionalItems",), + x, + index, + path, + dynamic_scope, + ) + _stopped(context, result) && return result + end + end + elseif items isa AbstractDict || items isa Bool + start = schema_dialect.modern_items ? prefix_count + 1 : 1 + for index in start:length(x) + _array_item!( + result, + context, + node, + ("items",), + x, + index, + path, + dynamic_scope, + ) + _stopped(context, result) && return result + end + end + contains = get(schema, "contains", nothing) + if keyword_applies(schema_dialect, "contains") && + (contains isa AbstractDict || contains isa Bool) + matches = BitSet() + for index in eachindex(x) + child = _child_result( + context, + node, + ("contains",), + x[index], + _array_path(path, index), + dynamic_scope, + ) + child.valid && push!(matches, index) + end + minimum = + keyword_applies(schema_dialect, "minContains") ? + get(schema, "minContains", 1) : 1 + maximum = + keyword_applies(schema_dialect, "maxContains") ? + get(schema, "maxContains", typemax(Int)) : typemax(Int) + if !(minimum <= length(matches) <= maximum) + _invalidate!(result, context, _issue(x, path, "contains", contains)) + else + if context.annotations && !isempty(matches) + result.items === nothing && (result.items = BitSet()) + union!(result.items::BitSet, matches) + end + end + end + return result +end + +function _array_applicators!( + result, + context, + node, + x, + schema, + schema_dialect, + path, + dynamic_scope, +) + return result +end + +function _unevaluated!( + result, + context, + node, + x::AbstractDict, + schema::AbstractDict, + path, + dynamic_scope, +) + unevaluated = get(schema, "unevaluatedProperties", nothing) + (unevaluated isa AbstractDict || unevaluated isa Bool) || return result + for (name, value) in x + result.properties !== nothing && + string(name) in result.properties && + continue + child = _child_result( + context, + node, + ("unevaluatedProperties",), + value, + _property_path(path, name), + dynamic_scope, + ) + _absorb!(result, context, child; annotations = false) + _stopped(context, result) && return result + _mark_property!(result, String(name)) + end + return result +end + +function _unevaluated!( + result, + context, + node, + x::AbstractVector, + schema::AbstractDict, + path, + dynamic_scope, +) + unevaluated = get(schema, "unevaluatedItems", nothing) + (unevaluated isa AbstractDict || unevaluated isa Bool) || return result + for index in eachindex(x) + result.items !== nothing && index in result.items && continue + _array_item!( + result, + context, + node, + ("unevaluatedItems",), + x, + index, + path, + dynamic_scope, + ) + _stopped(context, result) && return result + end + return result +end + +_unevaluated!(result, context, node, x, schema, path, dynamic_scope) = result + +function _evaluate_schema( + context::EvaluationContext, + node::Resources.NodeId, + x, + schema::Bool, + schema_dialect::Dialect, + path::EvaluationPath, + dynamic_scope, +) + result = EvaluationResult() + schema || _invalidate!(result, context, _issue(x, path, "schema", false)) + return result +end + +function _evaluate_schema( + context::EvaluationContext, + node::Resources.NodeId, + x, + schema::AbstractDict, + schema_dialect::Dialect, + path::EvaluationPath, + dynamic_scope, +) + result = EvaluationResult() + _references!( + result, + context, + node, + x, + schema, + schema_dialect, + path, + dynamic_scope, + ) || return result + schema_dialect.validation && + _simple_assertions!(result, context, x, schema, schema_dialect, path) + _stopped(context, result) && return result + if schema_dialect.applicator + _combinators!( + result, + context, + node, + x, + schema, + schema_dialect, + path, + dynamic_scope, + ) + _stopped(context, result) && return result + _object_applicators!( + result, + context, + node, + x, + schema, + schema_dialect, + path, + dynamic_scope, + ) + _stopped(context, result) && return result + _array_applicators!( + result, + context, + node, + x, + schema, + schema_dialect, + path, + dynamic_scope, + ) + _stopped(context, result) && return result + end + schema_dialect.unevaluated && + _unevaluated!(result, context, node, x, schema, path, dynamic_scope) + return result +end + +function _evaluate_compiled_node( + context::EvaluationContext, + compiled::CompiledNode, + x, + path::EvaluationPath, + dynamic_scope::Vector{Resources.ResourceId}, +) + node = compiled.id + context.evaluations += 1 + context.evaluations <= context.max_evaluations || throw( + EvaluationError( + node, + _path_string(path), + "the evaluation limit was reached", + ), + ) + context.depth += 1 + context.depth <= context.max_depth || begin + context.depth -= 1 + throw( + EvaluationError( + node, + _path_string(path), + "the evaluation depth limit was reached", + ), + ) + end + scoped = dynamic_scope + if isempty(dynamic_scope) || last(dynamic_scope) != node.resource + scoped = copy(dynamic_scope) + push!(scoped, node.resource) + end + if !context.tracks_cycles + try + return _evaluate_schema( + context, + node, + x, + compiled.value, + compiled.dialect, + path, + scoped, + ) + finally + context.depth -= 1 + end + end + scoped_key = + compiled.dialect.dynamic_refs || compiled.dialect.recursive_refs ? + Tuple(scoped) : () + active = (compiled.index, path, scoped_key) + if active in context.active + context.depth -= 1 + throw( + EvaluationError( + node, + _path_string(path), + "reference evaluation does not terminate", + ), + ) + end + push!(context.active, active) + try + return _evaluate_schema( + context, + node, + x, + compiled.value, + compiled.dialect, + path, + scoped, + ) + finally + delete!(context.active, active) + context.depth -= 1 + end +end + +function _evaluate_node( + context::EvaluationContext, + raw::Resources.NodeId, + x, + path::EvaluationPath, + dynamic_scope::Vector{Resources.ResourceId}, +) + return _evaluate_compiled_node( + context, + _compiled_node(context.schema, raw), + x, + path, + dynamic_scope, + ) +end + +function validate( + schema::CompiledSchema, + x; + fail_fast::Bool = true, + max_evaluations::Integer = 1_000_000, + max_issues::Integer = 10_000, + max_depth::Integer = 512, +) + context = EvaluationContext( + schema, + max_evaluations, + max_issues, + max_depth, + !fail_fast, + ) + result = _evaluate_node( + context, + schema.root, + x, + EvaluationPath(), + Resources.ResourceId[schema.root.resource], + ) + if fail_fast + return result.valid ? nothing : + first(result.issues::Vector{SingleIssue}) + end + return something(result.issues, SingleIssue[]) +end + +Base.isvalid(schema::CompiledSchema, x) = validate(schema, x) === nothing diff --git a/src/schema_engine/dialects.jl b/src/schema_engine/dialects.jl new file mode 100644 index 0000000..1fa9c16 --- /dev/null +++ b/src/schema_engine/dialects.jl @@ -0,0 +1,293 @@ +# Copyright (c) 2026: fredo-dedup, quinnj, and contributors +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +struct UnsupportedDialectError <: Exception + dialect::String +end + +function Base.showerror(io::IO, err::UnsupportedDialectError) + return print(io, "unsupported JSON Schema dialect ", repr(err.dialect)) +end + +"""The keyword and evaluation rules for a JSON Schema dialect.""" +struct Dialect + name::Symbol + uri::String + id_keyword::String + ref_siblings::Bool + modern_items::Bool + unevaluated::Bool + dynamic_refs::Bool + recursive_refs::Bool + applicator::Bool + validation::Bool +end + +const DRAFT4 = Dialect( + :draft4, + "http://json-schema.org/draft-04/schema", + "id", + false, + false, + false, + false, + false, + true, + true, +) +const DRAFT6 = Dialect( + :draft6, + "http://json-schema.org/draft-06/schema", + "\$id", + false, + false, + false, + false, + false, + true, + true, +) +const DRAFT7 = Dialect( + :draft7, + "http://json-schema.org/draft-07/schema", + "\$id", + false, + false, + false, + false, + false, + true, + true, +) +const DRAFT201909 = Dialect( + :draft201909, + "https://json-schema.org/draft/2019-09/schema", + "\$id", + true, + false, + true, + false, + true, + true, + true, +) +const DRAFT202012 = Dialect( + :draft202012, + "https://json-schema.org/draft/2020-12/schema", + "\$id", + true, + true, + true, + true, + false, + true, + true, +) + +const DIALECTS = Dict( + DRAFT4.name => DRAFT4, + DRAFT6.name => DRAFT6, + DRAFT7.name => DRAFT7, + DRAFT201909.name => DRAFT201909, + DRAFT202012.name => DRAFT202012, +) + +const COMMON_APPLICATOR_KEYWORDS = Set([ + "allOf", + "anyOf", + "oneOf", + "not", + "items", + "additionalProperties", + "properties", + "patternProperties", +]) + +const COMMON_VALIDATION_KEYWORDS = Set([ + "type", + "enum", + "multipleOf", + "maximum", + "exclusiveMaximum", + "minimum", + "exclusiveMinimum", + "maxLength", + "minLength", + "pattern", + "maxItems", + "minItems", + "uniqueItems", + "maxProperties", + "minProperties", + "required", +]) + +function keyword_applies(schema_dialect::Dialect, keyword::AbstractString) + name = schema_dialect.name + keyword in ("\$ref", "\$schema", schema_dialect.id_keyword) && return true + keyword == "\$defs" && return name in (:draft201909, :draft202012) + keyword == "definitions" && return name in (:draft4, :draft6, :draft7) + keyword == "\$anchor" && return name in (:draft201909, :draft202012) + keyword == "\$dynamicAnchor" && return schema_dialect.dynamic_refs + keyword == "\$dynamicRef" && return schema_dialect.dynamic_refs + keyword == "\$recursiveAnchor" && return schema_dialect.recursive_refs + keyword == "\$recursiveRef" && return schema_dialect.recursive_refs + keyword == "contentSchema" && return name in (:draft201909, :draft202012) + if schema_dialect.unevaluated && + keyword in ("unevaluatedItems", "unevaluatedProperties") + return true + end + if schema_dialect.applicator + keyword in COMMON_APPLICATOR_KEYWORDS && return true + keyword == "additionalItems" && return !schema_dialect.modern_items + keyword == "prefixItems" && return schema_dialect.modern_items + keyword in ("contains", "propertyNames") && return name != :draft4 + keyword in ("if", "then", "else") && + return name in (:draft7, :draft201909, :draft202012) + keyword == "dependencies" && return name in (:draft4, :draft6, :draft7) + keyword == "dependentSchemas" && + return name in (:draft201909, :draft202012) + end + if schema_dialect.validation + keyword in COMMON_VALIDATION_KEYWORDS && return true + keyword == "const" && return name != :draft4 + keyword == "dependentRequired" && + return name in (:draft201909, :draft202012) + keyword in ("minContains", "maxContains") && + return name in (:draft201909, :draft202012) + end + return false +end + +function _normalized_dialect_uri(uri::AbstractString) + return rstrip(String(uri), '#') +end + +function dialect(value::Dialect) + return value +end + +function dialect(value::Symbol) + return get( + () -> throw(UnsupportedDialectError(String(value))), + DIALECTS, + value, + ) +end + +function dialect(value::AbstractString) + normalized = _normalized_dialect_uri(value) + for candidate in values(DIALECTS) + _normalized_dialect_uri(candidate.uri) == normalized && return candidate + end + return throw(UnsupportedDialectError(String(value))) +end + +function dialect(schema::AbstractDict; default::Dialect = DRAFT7) + declared = get(schema, "\$schema", nothing) + declared === nothing && return default + declared isa AbstractString || + throw(UnsupportedDialectError(repr(declared))) + return dialect(declared) +end + +dialect(::Bool; default::Dialect = DRAFT7) = default + +const ECMA_GENERAL_CATEGORIES = Dict( + "Cased_Letter" => "L&", + "Close_Punctuation" => "Pe", + "Connector_Punctuation" => "Pc", + "Control" => "Cc", + "Currency_Symbol" => "Sc", + "Dash_Punctuation" => "Pd", + "Decimal_Number" => "Nd", + "Enclosing_Mark" => "Me", + "Final_Punctuation" => "Pf", + "Format" => "Cf", + "Initial_Punctuation" => "Pi", + "Letter" => "L", + "Letter_Number" => "Nl", + "Line_Separator" => "Zl", + "Lowercase_Letter" => "Ll", + "Mark" => "M", + "Math_Symbol" => "Sm", + "Modifier_Letter" => "Lm", + "Modifier_Symbol" => "Sk", + "Nonspacing_Mark" => "Mn", + "Number" => "N", + "Open_Punctuation" => "Ps", + "Other" => "C", + "Other_Letter" => "Lo", + "Other_Number" => "No", + "Other_Punctuation" => "Po", + "Other_Symbol" => "So", + "Paragraph_Separator" => "Zp", + "Private_Use" => "Co", + "Punctuation" => "P", + "Separator" => "Z", + "Space_Separator" => "Zs", + "Spacing_Mark" => "Mc", + "Surrogate" => "Cs", + "Symbol" => "S", + "Titlecase_Letter" => "Lt", + "Unassigned" => "Cn", + "Uppercase_Letter" => "Lu", +) + +function _ecma_property(property::AbstractString) + parts = split(property, '='; limit = 2) + if length(parts) == 1 + return get(ECMA_GENERAL_CATEGORIES, String(property), String(property)) + end + name, value = parts + if name in ("General_Category", "gc") + return get(ECMA_GENERAL_CATEGORIES, value, value) + elseif name in ("Script", "sc") + return "sc=$value" + elseif name in ("Script_Extensions", "scx") + return "scx=$value" + end + return String(property) +end + +function _ecma_pattern(pattern::AbstractString) + io = IOBuffer() + i = firstindex(pattern) + stop = lastindex(pattern) + while i <= stop + if pattern[i] != '\\' + write(io, pattern[i]) + i = nextind(pattern, i) + continue + end + slashes = 0 + while i <= stop && pattern[i] == '\\' + slashes += 1 + i = nextind(pattern, i) + end + for _ in 1:slashes + write(io, '\\') + end + isodd(slashes) || continue + i <= stop && pattern[i] in ('p', 'P') || continue + property_type = pattern[i] + brace = nextind(pattern, i) + brace <= stop && pattern[brace] == '{' || continue + closing = findnext('}', pattern, nextind(pattern, brace)) + closing === nothing && continue + property = SubString( + pattern, + nextind(pattern, brace), + prevind(pattern, closing), + ) + seek(io, position(io) - 1) + truncate(io, position(io)) + write(io, '\\', property_type, '{', _ecma_property(property), '}') + i = nextind(pattern, closing) + end + return String(take!(io)) +end + +_ecma_regex(pattern::AbstractString) = Regex(_ecma_pattern(pattern)) diff --git a/src/schema_engine/issues.jl b/src/schema_engine/issues.jl new file mode 100644 index 0000000..4c2cdc6 --- /dev/null +++ b/src/schema_engine/issues.jl @@ -0,0 +1,39 @@ +# Copyright (c) 2026: fredo-dedup, quinnj, and contributors +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +"""One JSON Schema validation issue.""" +struct SingleIssue + x::Any + path::String + reason::String + val::Any +end + +function Base.show(io::IO, issue::SingleIssue) + return println( + io, + """Validation failed: +path: $(isempty(issue.path) ? "top-level" : issue.path) +instance: $(issue.x) +schema key: $(issue.reason) +schema value: $(issue.val)""", + ) +end + +# JSON equality differs from Julia equality for booleans and numbers. JSON +# arrays and objects compare recursively with the same rule. +_isequal(x, y) = x == y +_isequal(::Bool, ::Number) = false +_isequal(::Number, ::Bool) = false +_isequal(x::Bool, y::Bool) = x == y + +function _isequal(x::AbstractVector, y::AbstractVector) + return length(x) == length(y) && all(_isequal.(x, y)) +end + +function _isequal(x::AbstractDict, y::AbstractDict) + return Set(keys(x)) == Set(keys(y)) && + all(_isequal(value, y[key]) for (key, value) in x) +end diff --git a/src/schema_engine/rebase.jl b/src/schema_engine/rebase.jl new file mode 100644 index 0000000..e6a20f0 --- /dev/null +++ b/src/schema_engine/rebase.jl @@ -0,0 +1,270 @@ +# Copyright (c) 2026: fredo-dedup, quinnj, and contributors +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +function _mutable_json(value::AbstractDict) + output = JSON.Object{String,Any}() + sizehint!(output, length(value)) + for (key, child) in value + output[String(key)] = _mutable_json(child) + end + return output +end + +_mutable_json(value::AbstractVector) = Any[_mutable_json(child) for child in value] +_mutable_json(value) = value + +function _reference_fragment(reference::AbstractString) + uri = URIs.URI(reference) + if isempty(uri.fragment) + return endswith(reference, '#') ? "#" : "" + end + return "#" * uri.fragment +end + +function _rebased_reference( + keyword::String, + raw::AbstractString, + target::Resources.NodeId, + resource_ids::Dict{Resources.ResourceId,Resources.ResourceId}, +) + resource = string(resource_ids[target.resource]) + if keyword == "\$ref" + pointer = string(target.pointer) + return isempty(pointer) ? resource : resource * "#" * pointer + end + # Dynamic and recursive references must keep their anchor fragment. A JSON + # Pointer can use the compiler's canonical target pointer. + parsed = Resources.Reference(target.resource, raw) + fragment = parsed.fragment + if fragment isa Resources.AnchorFragment + return resource * "#" * fragment.name + elseif fragment isa Resources.PointerFragment + pointer = string(target.pointer) + return isempty(pointer) ? resource * "#" : resource * "#" * pointer + end + return resource * _reference_fragment(raw) +end + +function _rebased_identifier( + raw::AbstractString, + node::CompiledNode, + resource_ids::Dict{Resources.ResourceId,Resources.ResourceId}, +) + return string(resource_ids[node.id.resource]) * _reference_fragment(raw) +end + +function _top_level_resources(registry::Resources.AbstractRegistry) + resources = Resources.Resource[ + resource for resource in values(getfield(registry, :resources)) + if isempty(resource.source.pointer) && + Resources.resource(registry, resource.retrieval).id == resource.id + ] + sort!(resources; by = resource -> string(resource.id)) + return resources +end + +function _normalize_resource_ids(registry, mapping) + output = Dict{Resources.ResourceId,Resources.ResourceId}() + for id in keys(getfield(registry, :resources)) + target = get(mapping, id, nothing) + target === nothing && throw( + ArgumentError("no replacement identifier was supplied for resource $(repr(string(id)))"), + ) + output[id] = target isa Resources.ResourceId ? target : Resources.ResourceId(target) + end + length(Set(values(output))) == length(output) || throw( + ArgumentError("replacement resource identifiers must be unique"), + ) + return output +end + +function _rewrite_resource_documents(template::CompiledSchema, resource_ids) + registry = template.registry + documents = Dict{Resources.ResourceId,Any}( + resource.id => _mutable_json(resource.contents) for + resource in _top_level_resources(registry) + ) + + seen = Set{Int}() + for node in values(getfield(template, :evaluation_nodes)) + node.index in seen && continue + push!(seen, node.index) + source = _source_node(registry, node.id) + owner = Resources.resource(registry, source.resource) + document = get(documents, owner.id, nothing) + document === nothing && continue + schema = Resources.resolve(document, source.pointer) + schema isa AbstractDict || continue + keyword = node.dialect.id_keyword + identifier = get(schema, keyword, nothing) + identifier isa AbstractString || continue + schema[keyword] = _rebased_identifier(identifier, node, resource_ids) + end + + for ((node, keyword), target) in getfield(template, :references) + source = _source_node(registry, node) + owner = Resources.resource(registry, source.resource) + document = get(documents, owner.id, nothing) + document === nothing && continue + schema = Resources.resolve(document, source.pointer) + schema isa AbstractDict || continue + raw = get(schema, keyword, nothing) + raw isa AbstractString || continue + schema[keyword] = _rebased_reference(keyword, raw, target, resource_ids) + end + return documents +end + +function _mapped_raw_node(resource_ids, node::Resources.NodeId) + return Resources.NodeId(resource_ids[node.resource], node.pointer) +end + +function _mapped_node(registry, resource_ids, node::Resources.NodeId) + canonical = Resources.canonical(registry, node) + return _mapped_raw_node(resource_ids, canonical) +end + +function _rebased_registry(registry, resource_ids, documents) + output = Resources.Registry() + resources = collect(values(getfield(registry, :resources))) + sort!(resources; by = resource -> (length(resource.source.pointer), string(resource.id))) + for original in resources + owner = Resources.resource(registry, original.source.resource) + id = resource_ids[original.id] + owner_id = resource_ids[owner.id] + contents = original.id == owner.id ? documents[owner.id] : + Resources.resolve(documents[owner.id], original.source.pointer) + Resources.register!( + output, + Resources.Resource( + id, + contents; + retrieval = owner_id, + source = Resources.NodeId(owner_id, original.source.pointer), + media_type = original.media_type, + ); + alias_retrieval = false, + ) + end + + dynamic = getfield(registry, :dynamic_anchors) + anchors = collect(getfield(registry, :anchors)) + sort!(anchors; by = entry -> (string(entry.first[1]), entry.first[2])) + for ((resource, name), node) in anchors + Resources.register_anchor!( + output, + resource_ids[resource], + name, + node.pointer; + dynamic = haskey(dynamic, (resource, name)), + ) + end + boundaries = collect(getfield(registry, :boundaries)) + sort!( + boundaries; + by = entry -> (string(entry.first.resource), string(entry.first.pointer)), + ) + for (source, target) in boundaries + Resources.register_boundary!( + output, + _mapped_raw_node(resource_ids, source), + _mapped_raw_node(resource_ids, target), + ) + end + return Resources.freeze(output) +end + +function _rebased_template(template, resource_ids, registry) + original_registry = template.registry + original_nodes = getfield(template, :evaluation_nodes) + nodes_by_index = Dict{Int,CompiledNode}() + for node in values(original_nodes) + haskey(nodes_by_index, node.index) && continue + id = _mapped_node(original_registry, resource_ids, node.id) + resource = Resources.resource(registry, id.resource) + value = Resources.resolve(resource.contents, id.pointer) + nodes_by_index[node.index] = CompiledNode(node.index, id, value, node.dialect) + end + + evaluation_nodes = Dict{Resources.NodeId,CompiledNode}() + for (id, node) in original_nodes + mapped = _mapped_node(original_registry, resource_ids, id) + evaluation_nodes[mapped] = nodes_by_index[node.index] + end + for node in values(nodes_by_index) + evaluation_nodes[node.id] = node + end + + dialects = Dict( + _mapped_node(original_registry, resource_ids, id) => dialect for + (id, dialect) in getfield(template, :dialects) + ) + transitions = Dict{Tuple{Int,Tuple{Vararg{String}}},CompiledNode}() + for (key, node) in getfield(template, :transitions) + transitions[key] = nodes_by_index[node.index] + end + references = Dict( + ( + _mapped_node(original_registry, resource_ids, source), + keyword, + ) => _mapped_node(original_registry, resource_ids, target) for + ((source, keyword), target) in getfield(template, :references) + ) + recursive_anchors = Set( + resource_ids[resource] for + resource in getfield(template, :recursive_anchors) + ) + root = _mapped_node(original_registry, resource_ids, template.root) + resource = Resources.resource(registry, root.resource) + data = Resources.resolve(resource.contents, root.pointer) + return CompiledSchema( + data, + template.dialect, + registry, + root, + dialects, + copy(getfield(template, :dialect_aliases)), + evaluation_nodes, + transitions, + template.uses_annotations, + recursive_anchors, + references, + copy(getfield(template, :regexes)), + Resources.DisabledRetriever(), + ) +end + +""" + rebase(schemas::CompiledSchemas, resource_ids) -> CompiledSchemas + +Create an equivalent, self-contained compiled graph under replacement resource +identifiers. `resource_ids` must map every canonical resource in `schemas` to a +unique replacement identifier. Schema identifiers and reference keywords are +rewritten from the compiled graph, so relative references, embedded resources, +anchors, dynamic references, and recursive references retain their meaning. + +Requested root identifiers from `schemas` remain valid lookup keys. The graph's +canonical roots and all serialized resource data use only replacement ids. +""" +function rebase(schemas::CompiledSchemas, mapping::AbstractDict) + template = getfield(schemas, :template) + registry = template.registry + resource_ids = _normalize_resource_ids(registry, mapping) + documents = _rewrite_resource_documents(template, resource_ids) + + rebased_registry = _rebased_registry(registry, resource_ids, documents) + rebased_template = _rebased_template(template, resource_ids, rebased_registry) + preserved = Dict{Resources.NodeId,Resources.NodeId}() + for (requested, raw_root) in getfield(schemas, :roots) + root = _mapped_node(registry, resource_ids, raw_root) + preserved[requested] = root + if haskey(resource_ids, requested.resource) + mapped_requested = _mapped_raw_node(resource_ids, requested) + preserved[mapped_requested] = root + end + preserved[root] = root + end + return CompiledSchemas(rebased_template, preserved) +end diff --git a/src/schema_engine/resources.jl b/src/schema_engine/resources.jl new file mode 100644 index 0000000..0b3ba82 --- /dev/null +++ b/src/schema_engine/resources.jl @@ -0,0 +1,927 @@ +# Copyright (c) 2026: fredo-dedup, quinnj, and contributors +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +"""Generic, non-mutating JSON resource identification and lookup.""" +module Resources + +import ..URIs + +struct PointerError <: Exception + pointer::String + token::Int + reason::String +end + +function Base.showerror(io::IO, err::PointerError) + location = err.token == 0 ? "root" : "token $(err.token)" + return print( + io, + "invalid JSON Pointer ", + repr(err.pointer), + " at ", + location, + ": ", + err.reason, + ) +end + +"""A parsed RFC 6901 JSON Pointer.""" +struct JSONPointer + tokens::Tuple{Vararg{String}} +end + +JSONPointer() = JSONPointer(()) + +function _unescape_token( + token::AbstractString, + pointer::AbstractString, + index::Int, +) + i = firstindex(token) + last = lastindex(token) + io = IOBuffer() + while i <= last + char = token[i] + if char != '~' + write(io, char) + i = nextind(token, i) + continue + end + next = nextind(token, i) + if next > last || !(token[next] in ('0', '1')) + throw(PointerError(String(pointer), index, "invalid '~' escape")) + end + write(io, token[next] == '0' ? '~' : '/') + i = nextind(token, next) + end + return String(take!(io)) +end + +function JSONPointer(pointer::AbstractString) + isempty(pointer) && return JSONPointer() + startswith(pointer, '/') || throw( + PointerError( + String(pointer), + 0, + "a non-empty pointer must start with '/'", + ), + ) + raw = split( + SubString(pointer, nextind(pointer, firstindex(pointer))), + '/'; + keepempty = true, + ) + tokens = ntuple(length(raw)) do i + return _unescape_token(raw[i], pointer, i) + end + return JSONPointer(tokens) +end + +function _escape_token(token::String) + return replace(replace(token, "~" => "~0"), "/" => "~1") +end + +function Base.string(pointer::JSONPointer) + isempty(pointer.tokens) && return "" + return "/" * join((_escape_token(token) for token in pointer.tokens), "/") +end + +function Base.show(io::IO, pointer::JSONPointer) + return print(io, "JSONPointer(", repr(string(pointer)), ")") +end +Base.length(pointer::JSONPointer) = length(pointer.tokens) +Base.isempty(pointer::JSONPointer) = isempty(pointer.tokens) +Base.iterate(pointer::JSONPointer, state...) = iterate(pointer.tokens, state...) +Base.getindex(pointer::JSONPointer, index::Integer) = pointer.tokens[index] + +function Base.:/(pointer::JSONPointer, token::AbstractString) + return JSONPointer((pointer.tokens..., String(token))) +end + +struct FrozenObject <: AbstractDict{String,Any} + entries::Tuple{Vararg{Pair{String,Any}}} + index::Dict{String,Int} +end + +Base.IteratorSize(::Type{<:FrozenObject}) = Base.HasLength() +Base.length(object::FrozenObject) = length(object.entries) +Base.iterate(object::FrozenObject, state...) = iterate(object.entries, state...) +Base.copy(object::FrozenObject) = object + +function Base.getproperty(object::FrozenObject, name::Symbol) + name === :index && return copy(getfield(object, :index)) + return getfield(object, name) +end + +function Base.haskey(object::FrozenObject, key) + return haskey(getfield(object, :index), key) +end + +function Base.getindex(object::FrozenObject, key) + position = getfield(object, :index)[key] + return getfield(object, :entries)[position].second +end + +function Base.get(object::FrozenObject, key, default) + position = get(getfield(object, :index), key, 0) + return position == 0 ? default : getfield(object, :entries)[position].second +end + +function Base.get(default::Union{Function,Type}, object::FrozenObject, key) + position = get(getfield(object, :index), key, 0) + return position == 0 ? default() : + getfield(object, :entries)[position].second +end + +struct FrozenArray <: AbstractVector{Any} + entries::Tuple{Vararg{Any}} +end + +Base.IndexStyle(::Type{FrozenArray}) = IndexLinear() +Base.size(array::FrozenArray) = (length(array.entries),) +Base.getindex(array::FrozenArray, index::Int) = array.entries[index] +Base.copy(array::FrozenArray) = array + +"""Create a read-only recursive view of a parsed JSON value.""" +freeze(value::FrozenObject) = value +freeze(value::FrozenArray) = value + +function freeze(value::AbstractDict) + entries = Pair{String,Any}[] + index = Dict{String,Int}() + sizehint!(entries, length(value)) + sizehint!(index, length(value)) + for (key, item) in value + key isa AbstractString || + throw(ArgumentError("JSON object keys must be strings")) + normalized = String(key) + push!(entries, normalized => freeze(item)) + index[normalized] = length(entries) + end + return FrozenObject(Tuple(entries), index) +end + +function freeze(value::AbstractVector) + entries = Any[] + sizehint!(entries, length(value)) + for item in value + push!(entries, freeze(item)) + end + return FrozenArray(Tuple(entries)) +end + +freeze(value) = value + +function _array_index(pointer::JSONPointer, token::String, index::Int) + isempty(token) && throw( + PointerError(string(pointer), index, "an array index cannot be empty"), + ) + occursin(r"^(0|[1-9][0-9]*)$", token) || throw( + PointerError( + string(pointer), + index, + "expected an unsigned decimal array index", + ), + ) + value = tryparse(Int, token) + value === nothing && throw( + PointerError( + string(pointer), + index, + "expected a non-negative integer array index", + ), + ) + value < 0 && throw( + PointerError( + string(pointer), + index, + "expected a non-negative integer array index", + ), + ) + return value + 1 +end + +function resolve(document, pointer::JSONPointer) + value = document + for (index, token) in enumerate(pointer.tokens) + if value isa AbstractDict + haskey(value, token) || throw( + PointerError( + string(pointer), + index, + "object member $(repr(token)) does not exist", + ), + ) + value = value[token] + elseif value isa AbstractVector + julia_index = _array_index(pointer, token, index) + checkbounds(Bool, value, julia_index) || throw( + PointerError( + string(pointer), + index, + "array index $(repr(token)) is out of bounds", + ), + ) + value = value[julia_index] + else + throw( + PointerError( + string(pointer), + index, + "cannot traverse a $(typeof(value)) value", + ), + ) + end + end + return value +end + +"""A canonical, fragment-free resource identifier.""" +struct ResourceId + uri::URIs.URI + text::String + + function ResourceId(uri::URIs.URI) + isempty(uri.fragment) || throw( + ArgumentError("a resource identifier cannot contain a fragment"), + ) + scheme = lowercase(uri.scheme) + host = lowercase(uri.host) + path = _remove_dot_segments(_normalize_percent_encoding(uri.path)) + if scheme in ("http", "https") && !isempty(host) && isempty(path) + path = "/" + end + normalized = _build_uri( + scheme, + _normalize_percent_encoding(uri.userinfo), + host, + _normalized_port(scheme, uri.port), + path, + _normalize_percent_encoding(uri.query), + _has_authority(uri), + ) + return new(normalized, string(normalized)) + end +end + +function _is_unreserved(byte::UInt8) + return UInt8('A') <= byte <= UInt8('Z') || + UInt8('a') <= byte <= UInt8('z') || + UInt8('0') <= byte <= UInt8('9') || + byte == UInt8('-') || + byte == UInt8('.') || + byte == UInt8('_') || + byte == UInt8('~') +end + +function _normalize_percent_encoding(value::AbstractString) + isempty(value) && return value + bytes = codeunits(value) + output = IOBuffer() + index = 1 + while index <= length(bytes) + if bytes[index] == UInt8('%') && index + 2 <= length(bytes) + encoded = tryparse( + UInt8, + String(copy(bytes[(index+1):(index+2)])); + base = 16, + ) + if encoded !== nothing + if _is_unreserved(encoded) + write(output, encoded) + else + print( + output, + '%', + uppercase(string(encoded; base = 16, pad = 2)), + ) + end + index += 3 + continue + end + end + write(output, bytes[index]) + index += 1 + end + return String(take!(output)) +end + +function _remove_last_segment(path::String) + separator = findlast(==('/'), path) + separator === nothing && return "" + separator == firstindex(path) && return "" + return String(SubString(path, firstindex(path), prevind(path, separator))) +end + +function _move_first_segment(path::String) + start = firstindex(path) + search = path[start] == '/' ? nextind(path, start) : start + separator = findnext(==('/'), path, search) + separator === nothing && return (path, "") + segment = String(SubString(path, start, prevind(path, separator))) + return (segment, String(SubString(path, separator))) +end + +function _remove_dot_segments(path::AbstractString) + input = String(path) + output = "" + while !isempty(input) + if startswith(input, "../") + input = input[4:end] + elseif startswith(input, "./") + input = input[3:end] + elseif startswith(input, "/./") + input = input[3:end] + elseif input == "/." + input = "/" + elseif startswith(input, "/../") + input = input[4:end] + output = _remove_last_segment(output) + elseif input == "/.." + input = "/" + output = _remove_last_segment(output) + elseif input in (".", "..") + input = "" + else + segment, input = _move_first_segment(input) + output *= segment + end + end + return output +end + +function _has_authority(uri::URIs.URI) + lowercase(uri.scheme) in URIs.uses_authority && return true + (!isempty(uri.userinfo) || !isempty(uri.host) || !isempty(uri.port)) && + return true + text = string(uri) + isempty(uri.scheme) && return startswith(text, "//") + separator = findfirst(==(':'), text) + separator === nothing && return false + return startswith(SubString(text, nextind(text, separator)), "//") +end + +function _build_uri(scheme, userinfo, host, port, path, query, authority::Bool) + output = IOBuffer() + isempty(scheme) || print(output, scheme, ':') + if authority + print(output, "//") + isempty(userinfo) || print(output, userinfo, '@') + if occursin(':', host) && !startswith(host, '[') + print(output, '[', host, ']') + else + print(output, host) + end + isempty(port) || print(output, ':', port) + end + print(output, path) + isempty(query) || print(output, '?', query) + return URIs.URI(String(take!(output))) +end + +function _normalized_port(scheme::AbstractString, port::AbstractString) + normalized_scheme = lowercase(scheme) + if (normalized_scheme == "http" && port == "80") || + (normalized_scheme == "https" && port == "443") + return "" + end + return String(port) +end + +ResourceId(uri::AbstractString) = ResourceId(URIs.URI(uri)) +Base.string(id::ResourceId) = id.text +Base.:(==)(left::ResourceId, right::ResourceId) = left.text == right.text +function Base.isequal(left::ResourceId, right::ResourceId) + return isequal(left.text, right.text) +end +Base.hash(id::ResourceId, hash::UInt) = Base.hash(id.text, hash) +function Base.show(io::IO, id::ResourceId) + return print(io, "ResourceId(", repr(string(id)), ")") +end + +abstract type Fragment end + +struct RootFragment <: Fragment end + +struct PointerFragment <: Fragment + pointer::JSONPointer +end + +struct AnchorFragment <: Fragment + name::String + + function AnchorFragment(name::AbstractString) + isempty(name) && throw(ArgumentError("an anchor name cannot be empty")) + return new(String(name)) + end +end + +"""An absolute resource reference with a parsed root, pointer, or anchor fragment.""" +struct Reference + resource::ResourceId + fragment::Fragment +end + +function _without_fragment(uri::URIs.URI) + return _build_uri( + uri.scheme, + uri.userinfo, + uri.host, + uri.port, + uri.path, + uri.query, + _has_authority(uri), + ) +end + +function _fragment(uri::URIs.URI) + fragment = URIs.unescapeuri(uri.fragment) + isempty(fragment) && return RootFragment() + startswith(fragment, '/') && return PointerFragment(JSONPointer(fragment)) + return AnchorFragment(fragment) +end + +function Reference(base::ResourceId, reference::AbstractString) + resolved = URIs.resolvereference(base.uri, URIs.URI(reference)) + return Reference( + ResourceId(_without_fragment(resolved)), + _fragment(resolved), + ) +end + +struct NodeId + resource::ResourceId + pointer::JSONPointer +end + +"""A read-only JSON resource with canonical and retrieval identifiers.""" +struct Resource{T} + id::ResourceId + retrieval::ResourceId + source::NodeId + contents::T + media_type::Union{Nothing,String} + + function Resource( + id::ResourceId, + retrieval::ResourceId, + source::NodeId, + contents, + media_type::Union{Nothing,AbstractString}, + ) + frozen = freeze(contents) + normalized_media_type = + media_type === nothing ? nothing : String(media_type) + return new{typeof(frozen)}( + id, + retrieval, + source, + frozen, + normalized_media_type, + ) + end +end + +function Resource( + id::ResourceId, + contents; + retrieval::ResourceId = id, + source::NodeId = NodeId(retrieval, JSONPointer()), + media_type::Union{Nothing,AbstractString} = nothing, +) + return Resource(id, retrieval, source, contents, media_type) +end + +struct DuplicateResourceError <: Exception + id::ResourceId +end + +function Base.showerror(io::IO, err::DuplicateResourceError) + return print( + io, + "resource ", + repr(string(err.id)), + " is already registered", + ) +end + +struct MissingResourceError <: Exception + id::ResourceId +end + +function Base.showerror(io::IO, err::MissingResourceError) + return print(io, "resource ", repr(string(err.id)), " is not registered") +end + +struct MissingAnchorError <: Exception + resource::ResourceId + anchor::String +end + +function Base.showerror(io::IO, err::MissingAnchorError) + return print( + io, + "anchor ", + repr(err.anchor), + " is not registered in resource ", + repr(string(err.resource)), + ) +end + +abstract type AbstractRegistry end + +"""A registry builder for immutable resources, aliases, and plain-name anchors.""" +mutable struct Registry <: AbstractRegistry + resources::Dict{ResourceId,Resource} + aliases::Dict{ResourceId,ResourceId} + anchors::Dict{Tuple{ResourceId,String},NodeId} + dynamic_anchors::Dict{Tuple{ResourceId,String},NodeId} + boundaries::Dict{NodeId,NodeId} +end + +"""A read-only snapshot of a resource registry.""" +struct FrozenRegistry <: AbstractRegistry + resources::Dict{ResourceId,Resource} + aliases::Dict{ResourceId,ResourceId} + anchors::Dict{Tuple{ResourceId,String},NodeId} + dynamic_anchors::Dict{Tuple{ResourceId,String},NodeId} + boundaries::Dict{NodeId,NodeId} + + function FrozenRegistry( + resources, + aliases, + anchors, + dynamic_anchors, + boundaries, + ) + return new( + copy(resources), + copy(aliases), + copy(anchors), + copy(dynamic_anchors), + copy(boundaries), + ) + end +end + +function freeze(registry::Registry) + return FrozenRegistry( + registry.resources, + registry.aliases, + registry.anchors, + registry.dynamic_anchors, + registry.boundaries, + ) +end + +function Base.getproperty(registry::FrozenRegistry, name::Symbol) + name in (:resources, :aliases, :anchors, :dynamic_anchors, :boundaries) && + return copy(getfield(registry, name)) + return getfield(registry, name) +end + +function Registry() + return Registry( + Dict{ResourceId,Resource}(), + Dict{ResourceId,ResourceId}(), + Dict{Tuple{ResourceId,String},NodeId}(), + Dict{Tuple{ResourceId,String},NodeId}(), + Dict{NodeId,NodeId}(), + ) +end + +function _canonical_id(registry::AbstractRegistry, id::ResourceId) + return get(getfield(registry, :aliases), id, id) +end + +function register!( + registry::Registry, + resource::Resource; + aliases = ResourceId[], + anchors = Pair{String,JSONPointer}[], + alias_retrieval::Bool = true, +) + ids = ResourceId[resource.id] + alias_retrieval && push!(ids, resource.retrieval) + append!(ids, aliases) + unique!(ids) + for id in ids + canonical = _canonical_id(registry, id) + if haskey(registry.resources, canonical) || haskey(registry.aliases, id) + throw(DuplicateResourceError(id)) + end + end + anchor_entries = Pair{Tuple{ResourceId,String},NodeId}[] + for (name, pointer) in anchors + normalized_name = AnchorFragment(name).name + key = (resource.id, normalized_name) + if haskey(registry.anchors, key) || + any(entry -> entry.first == key, anchor_entries) + throw(ArgumentError("duplicate anchor $(repr(normalized_name))")) + end + resolve(resource.contents, pointer) + push!(anchor_entries, key => NodeId(resource.id, pointer)) + end + registry.resources[resource.id] = resource + for id in ids + id == resource.id && continue + registry.aliases[id] = resource.id + end + for entry in anchor_entries + registry.anchors[entry.first] = entry.second + end + return resource +end + +function register_alias!( + registry::Registry, + alias::ResourceId, + target::ResourceId, +) + registered = resource(registry, target) + alias == registered.id && return registered.id + if haskey(registry.resources, alias) || haskey(registry.aliases, alias) + throw(DuplicateResourceError(alias)) + end + registry.aliases[alias] = registered.id + return registered.id +end + +function register_anchor!( + registry::Registry, + resource_id::ResourceId, + name::AbstractString, + pointer::JSONPointer; + dynamic::Bool = false, +) + registered = resource(registry, resource_id) + normalized_name = AnchorFragment(name).name + key = (registered.id, normalized_name) + haskey(registry.anchors, key) && + throw(ArgumentError("duplicate anchor $(repr(normalized_name))")) + resolve(registered.contents, pointer) + node = NodeId(registered.id, pointer) + registry.anchors[key] = node + dynamic && (registry.dynamic_anchors[key] = node) + return node +end + +function register_boundary!(registry::Registry, source::NodeId, target::NodeId) + source == target && return target + haskey(registry.boundaries, source) && throw( + ArgumentError( + "resource boundary $(repr(source)) is already registered", + ), + ) + resolve(resource(registry, source.resource).contents, source.pointer) + resource(registry, target.resource) + isempty(target.pointer) || throw( + ArgumentError("a resource boundary target must be a resource root"), + ) + registry.boundaries[source] = target + return target +end + +function canonical(registry::AbstractRegistry, node::NodeId) + canonical_id = _canonical_id(registry, node.resource) + boundaries = getfield(registry, :boundaries) + normalized = + canonical_id == node.resource ? node : + NodeId(canonical_id, node.pointer) + isempty(boundaries) && return normalized + current = NodeId(canonical_id, JSONPointer()) + current = get(boundaries, current, current) + for token in node.pointer + current = NodeId(current.resource, current.pointer / token) + current = get(boundaries, current, current) + end + return current +end + +function resource(registry::AbstractRegistry, id::ResourceId) + canonical = _canonical_id(registry, id) + return get( + () -> throw(MissingResourceError(id)), + getfield(registry, :resources), + canonical, + ) +end + +function Base.haskey(registry::AbstractRegistry, id::ResourceId) + return haskey(getfield(registry, :resources), _canonical_id(registry, id)) +end + +Base.length(registry::AbstractRegistry) = length(getfield(registry, :resources)) +Base.isempty(registry::AbstractRegistry) = isempty(getfield(registry, :resources)) + +struct ResolvedNode{T} + id::NodeId + value::T +end + +function resolve(registry::AbstractRegistry, reference::Reference) + registered = resource(registry, reference.resource) + fragment = reference.fragment + if fragment isa RootFragment + id = canonical(registry, NodeId(registered.id, JSONPointer())) + elseif fragment isa PointerFragment + id = canonical(registry, NodeId(registered.id, fragment.pointer)) + else + key = (registered.id, fragment.name) + id = get( + () -> throw(MissingAnchorError(registered.id, fragment.name)), + getfield(registry, :anchors), + key, + ) + end + canonical_resource = resource(registry, id.resource) + return ResolvedNode(id, resolve(canonical_resource.contents, id.pointer)) +end + +function dynamic_anchor( + registry::AbstractRegistry, + resource_id::ResourceId, + name::AbstractString, +) + registered = resource(registry, resource_id) + return get( + getfield(registry, :dynamic_anchors), + (registered.id, String(name)), + nothing, + ) +end + +abstract type AbstractRetriever end + +struct RetrievalError <: Exception + id::ResourceId + reason::String +end + +function Base.showerror(io::IO, err::RetrievalError) + return print(io, "cannot retrieve ", repr(string(err.id)), ": ", err.reason) +end + +struct RetrievedResource + id::ResourceId + bytes::Vector{UInt8} + media_type::Union{Nothing,String} + + function RetrievedResource( + id::ResourceId, + bytes::AbstractVector{UInt8}, + media_type::Union{Nothing,AbstractString}, + ) + normalized_media_type = + media_type === nothing ? nothing : String(media_type) + return new(id, copy(bytes), normalized_media_type) + end +end + +function Base.getproperty(resource::RetrievedResource, name::Symbol) + name === :bytes && return copy(getfield(resource, :bytes)) + return getfield(resource, name) +end + +function RetrievedResource( + id::ResourceId, + bytes::AbstractVector{UInt8}; + media_type::Union{Nothing,AbstractString} = nothing, +) + return RetrievedResource(id, bytes, media_type) +end + +struct DisabledRetriever <: AbstractRetriever end + +function retrieve(::DisabledRetriever, id::ResourceId) + return throw(RetrievalError(id, "external retrieval is disabled")) +end + +struct MemoryRetriever <: AbstractRetriever + resources::Dict{ResourceId,RetrievedResource} + + function MemoryRetriever(resources::Dict{ResourceId,RetrievedResource}) + copied = Dict( + id => + RetrievedResource(value.id, value.bytes, value.media_type) + for (id, value) in resources + ) + return new(copied) + end +end + +function Base.getproperty(retriever::MemoryRetriever, name::Symbol) + name === :resources && return copy(getfield(retriever, :resources)) + return getfield(retriever, name) +end + +MemoryRetriever() = MemoryRetriever(Dict{ResourceId,RetrievedResource}()) + +function MemoryRetriever(resources::AbstractDict) + normalized = Dict{ResourceId,RetrievedResource}() + for (raw_id, value) in resources + id = raw_id isa ResourceId ? raw_id : ResourceId(raw_id) + if value isa RetrievedResource + normalized[id] = value + elseif value isa AbstractString + normalized[id] = + RetrievedResource(id, Vector{UInt8}(codeunits(value))) + elseif value isa AbstractVector{UInt8} + normalized[id] = RetrievedResource(id, value) + else + throw( + ArgumentError( + "memory resources must contain strings, bytes, or RetrievedResource values", + ), + ) + end + end + return MemoryRetriever(normalized) +end + +function retrieve(retriever::MemoryRetriever, id::ResourceId) + found = get( + () -> + throw(RetrievalError(id, "resource is not present in memory")), + getfield(retriever, :resources), + id, + ) + return RetrievedResource( + found.id, + found.bytes; + media_type = found.media_type, + ) +end + +struct FileRetriever <: AbstractRetriever + roots::Vector{String} + max_bytes::Int + + function FileRetriever( + roots::AbstractVector{<:AbstractString}; + max_bytes::Integer = 16 * 1024 * 1024, + ) + max_bytes > 0 || throw(ArgumentError("max_bytes must be positive")) + normalized = String[] + for root in roots + path = realpath(abspath(expanduser(root))) + isdir(path) || throw( + ArgumentError("file root $(repr(path)) is not a directory"), + ) + push!(normalized, path) + end + isempty(normalized) && + throw(ArgumentError("at least one file root is required")) + return new(unique(normalized), Int(max_bytes)) + end +end + +function FileRetriever(root::AbstractString; kwargs...) + return FileRetriever([root]; kwargs...) +end + +function _is_within(path::String, root::String) + path == root && return true + separator = Sys.iswindows() ? '\\' : '/' + return startswith(path, rstrip(root, separator) * separator) +end + +function retrieve(retriever::FileRetriever, id::ResourceId) + uri = id.uri + (isempty(uri.scheme) || lowercase(uri.scheme) == "file") || + throw(RetrievalError(id, "the URI scheme is not file")) + isempty(uri.host) || + lowercase(uri.host) == "localhost" || + throw(RetrievalError(id, "remote file hosts are not allowed")) + isempty(uri.query) || + throw(RetrievalError(id, "file URIs cannot contain a query")) + path = try + realpath(abspath(URIs.unescapeuri(uri.path))) + catch err + throw(RetrievalError(id, sprint(showerror, err))) + end + any(root -> _is_within(path, root), retriever.roots) || + throw(RetrievalError(id, "the path is outside the allowed roots")) + isfile(path) || throw(RetrievalError(id, "the path is not a regular file")) + bytes = try + open(path, "r") do io + filesize(io) <= retriever.max_bytes || throw( + RetrievalError( + id, + "the file exceeds the $(retriever.max_bytes)-byte limit", + ), + ) + return read(io) + end + catch err + err isa RetrievalError && rethrow() + throw(RetrievalError(id, sprint(showerror, err))) + end + media_type = + endswith(lowercase(path), ".json") ? "application/schema+json" : nothing + return RetrievedResource(id, bytes; media_type) +end + +end diff --git a/src/schemas.jl b/src/schemas.jl new file mode 100644 index 0000000..4fda421 --- /dev/null +++ b/src/schemas.jl @@ -0,0 +1,93 @@ +# Julia type -> JSON Schema (2020-12 dialect, as used by OpenAPI 3.1+). +# Named struct types are registered once under #/components/schemas and +# referenced by $ref everywhere they appear. + +function obj(pairs::Pair...) + o = JSON.Object{String,Any}() + for (k, v) in pairs + o[String(k)] = v + end + return o +end + +emptyschema() = JSON.Object{String,Any}() # {} matches any value + +""" +Accumulates `#/components/schemas` entries while a document is built. Named +Julia struct types are registered once (by `nameof`, deduped) and referenced. +""" +struct SchemaRegistry + schemas::JSON.Object{String,Any} + seen::IdDict{Any,String} +end +SchemaRegistry() = SchemaRegistry(JSON.Object{String,Any}(), IdDict{Any,String}()) + +uniontypes(T) = T isa Union ? [uniontypes(T.a); uniontypes(T.b)] : [T] + +""" + schemaof(registry, T) -> JSON.Object + +The JSON Schema for a Julia type. Primitives map directly; `Union`s become +`oneOf` (with `Union{Nothing, T}` including a null schema); `Vector`/`Dict` +map to arrays/objects; `NamedTuple`s become inline object schemas; named +structs are registered in the components registry and referenced with `\$ref`; +`Any` and abstract types become the empty (match-anything) schema. +""" +function schemaof(reg::SchemaRegistry, ::Type{T}) where {T} + T === Any && return emptyschema() + T === Nothing && return obj("type" => "null") + T === Missing && return obj("type" => "null") + T === Bool && return obj("type" => "boolean") + T === Int64 && return obj("type" => "integer", "format" => "int64") + T === Int32 && return obj("type" => "integer", "format" => "int32") + T <: Integer && return obj("type" => "integer") + T === Float64 && return obj("type" => "number", "format" => "double") + T <: Real && return obj("type" => "number") + T === Dates.Date && return obj("type" => "string", "format" => "date") + T === Dates.DateTime && return obj("type" => "string", "format" => "date-time") + T === Dates.Time && return obj("type" => "string", "format" => "time") + (T <: AbstractString || T === Symbol || T === Char) && return obj("type" => "string") + T isa Union && return obj("oneOf" => Any[schemaof(reg, t) for t in uniontypes(T)]) + T <: Base.Enum && + return obj("type" => "string", "enum" => Any[string(i) for i in instances(T)]) + T <: AbstractVector && + return obj("type" => "array", "items" => schemaof(reg, eltype(T))) + T <: AbstractDict && + return obj("type" => "object", "additionalProperties" => schemaof(reg, valtype(T))) + T <: Tuple && return obj("type" => "array") + T <: NamedTuple && isconcretetype(T) && return objectschema(reg, T) + if isstructtype(T) && isconcretetype(T) + name = get(reg.seen, T, nothing) + if name === nothing + name = string(nameof(T)) + i = 2 + while haskey(reg.schemas, name) + name = string(nameof(T), "_", i) + i += 1 + end + reg.seen[T] = name + # register before recursing so self-referential types terminate + reg.schemas[name] = emptyschema() + reg.schemas[name] = objectschema(reg, T) + end + return obj("\$ref" => "#/components/schemas/$name") + end + return emptyschema() +end + +function objectschema(reg::SchemaRegistry, ::Type{T}) where {T} + props = JSON.Object{String,Any}() + required = String[] + for (fname, ftype) in zip(fieldnames(T), fieldtypes(T)) + props[string(fname)] = schemaof(reg, ftype) + # Union{Nothing, ...} (and Any) fields are optional; everything else required + Nothing <: ftype || push!(required, string(fname)) + end + s = obj( + "type" => "object", + "properties" => props, + "additionalProperties" => false, + ) + isempty(required) || (s["required"] = required) + return s +end diff --git a/src/server.jl b/src/server.jl deleted file mode 100644 index 4029555..0000000 --- a/src/server.jl +++ /dev/null @@ -1,186 +0,0 @@ -module Servers - -using JSON -using HTTP - -import ..OpenAPI: APIModel, ValidationException, from_json, to_json, deep_object_to_array, StyleCtx, is_deep_explode - -function middleware(impl, read, validate, invoke; - init=nothing, - pre_validation=nothing, - pre_invoke=nothing, - post_invoke=nothing - ) - handler = req -> (invoke(impl; post_invoke=post_invoke))(req) - if !isnothing(pre_invoke) - handler = pre_invoke(handler) - end - handler = validate(handler) - if !isnothing(pre_validation) - handler = pre_validation(handler) - end - handler = read(handler) - if !isnothing(init) - handler = init(handler) - end - return handler -end - -############################## -# server parameter conversions -############################## -struct Param - keylist::Vector{String} - value::String -end - -function parse_query_dict(query_dict::Dict{String, String})::Vector{Param} - params = Vector{Param}() - for (key, value) in query_dict - keylist = replace.(split(key, "["), "]"=>"") - push!(params, Param(keylist, value)) - end - - return params -end - -function deep_dict_repr(qp::Dict) - params = parse_query_dict(qp) - deserialized_dict = Dict{String, Any}() - for param in params - current = deserialized_dict - for part in param.keylist[1:end-1] - current = get!(current, part) do - return Dict{String, Any}() - end - end - current[param.keylist[end]] = param.value - end - return deserialized_dict -end - -function get_param(source::Dict, name::String, required::Bool) - val = get(source, name, nothing) - if isnothing(val) - # HTTP header field names are case-insensitive, and some HTTP.jl versions - # canonicalize incoming request header names (e.g. "api_key" -> "Api_key"). - # Fall back to a case-insensitive match so header params resolve regardless - # of the HTTP.jl version. The exact lookup above wins first, so query/path - # params (whose dicts are not canonicalized) are unaffected. - lname = lowercase(name) - for (k, v) in source - if lowercase(k) == lname - val = v - break - end - end - end - if required && isnothing(val) - throw(ValidationException("required parameter \"$name\" missing")) - end - return val -end - -function get_param(source::Vector{HTTP.Multipart}, name::String, required::Bool) - ind = findfirst(x -> x.name == name, source) - if required && isnothing(ind) - throw(ValidationException("required parameter \"$name\" missing")) - elseif isnothing(ind) - return nothing - else - return source[ind] - end -end - -function to_param_type(::Type{T}, strval::String; stylectx=nothing) where {T <: Number} - parse(T, strval) -end - -to_param_type(::Type{T}, val::T; stylectx=nothing) where {T} = val -to_param_type(::Type{T}, ::Nothing; stylectx=nothing) where {T} = nothing -to_param_type(::Type{String}, val::Vector{UInt8}; stylectx=nothing) = String(copy(val)) -to_param_type(::Type{Vector{UInt8}}, val::String; stylectx=nothing) = convert(Vector{UInt8}, copy(codeunits(val))) -to_param_type(::Type{Vector{T}}, val::Vector{T}, _collection_format::Union{String,Nothing}; stylectx=nothing) where {T} = val -to_param_type(::Type{Vector{T}}, json::Vector{Any}; stylectx=nothing) where {T} = [to_param_type(T, x; stylectx) for x in json] - -function to_param_type(::Type{Vector{T}}, json::AbstractDict{String, Any}; stylectx=nothing) where {T} - if !isnothing(stylectx) && is_deep_explode(stylectx) - cvt = deep_object_to_array(json) - if isa(cvt, Vector) - return to_param_type(Vector{T}, cvt; stylectx) - end - end - error("Unable to convert $json to $(Vector{T})") -end - -function to_param_type(::Type{T}, strval::String; stylectx=nothing) where {T <: APIModel} - from_json(T, JSON.parse(strval); stylectx) -end - -function to_param_type(::Type{T}, json::AbstractDict{String,Any}; stylectx=nothing) where {T <: APIModel} - from_json(T, json; stylectx) -end - -function to_param_type(::Type{Vector{T}}, strval::String, delim::String; stylectx=nothing) where {T} - elems = string.(strip.(split(strval, delim))) - return map(x->to_param_type(T, x; stylectx), elems) -end - -function to_param_type(::Type{Vector{T}}, strval::String; stylectx=nothing) where {T} - elems = JSON.parse(strval) - return map(x->to_param_type(T, x; stylectx), elems) -end - -function to_param(T, source::Dict, name::String; required::Bool=false, collection_format::Union{String,Nothing}=",", multipart::Bool=false, isfile::Bool=false, style::String="form", is_explode::Bool=true, location=:query) - deep_explode = style == "deepObject" && is_explode - if deep_explode - source = deep_dict_repr(source) - end - param = get_param(source, name, required) - if param === nothing - return nothing - end - if multipart - # param is a Multipart - param = isfile ? param.data : String(param.data) - end - if deep_explode - return to_param_type(T, param; stylectx=StyleCtx(location, style, is_explode)) - end - if T <: Vector - to_param_type(T, param, collection_format) - else - to_param_type(T, param) - end -end - -function to_param(T, source::Vector{HTTP.Multipart}, name::String; required::Bool=false, collection_format::Union{String,Nothing}=",", multipart::Bool=false, isfile::Bool=false) - param = get_param(source, name, required) - if param === nothing - return nothing - end - if multipart - # param is a Multipart - param = isfile ? take!(param.data) : String(take!(param.data)) - end - if T <: Vector - return to_param_type(T, param, collection_format) - else - return to_param_type(T, param) - end -end - -function HTTP.Response(code::Integer, o::APIModel) - return HTTP.Response(code, [Pair("Content-Type", "application/json")], to_json(o)) -end - -server_response(resp::HTTP.Response) = resp -server_response(::Nothing) = server_response("") -server_response(ret::Vector{UInt8}) = - HTTP.Response(200, ["Content-Type" => "application/octet-stream"], body=ret) -server_response(ret) = - server_response(to_json(ret), [Pair("Content-Type", "application/json")]) -server_response(resp::AbstractString, headers=HTTP.Headers()) = - HTTP.Response(200, headers, body=resp) - -end # module Servers diff --git a/src/source_locations.jl b/src/source_locations.jl new file mode 100644 index 0000000..4c60b57 --- /dev/null +++ b/src/source_locations.jl @@ -0,0 +1,302 @@ +mutable struct JSONLocationCursor + bytes::Vector{UInt8} + index::Int + line::Int + column::Int + after_cr::Bool + locations::Dict{Resources.JSONPointer,SourcePosition} +end + +function JSONLocationCursor(bytes::AbstractVector{UInt8}) + copied = Vector{UInt8}(bytes) + cursor = JSONLocationCursor( + copied, + 1, + 1, + 1, + false, + Dict{Resources.JSONPointer,SourcePosition}(), + ) + if length(copied) >= 3 && copied[1:3] == UInt8[0xef, 0xbb, 0xbf] + cursor.index = 4 + end + return cursor +end + +_json_eof(cursor::JSONLocationCursor) = cursor.index > length(cursor.bytes) +_json_peek(cursor::JSONLocationCursor) = cursor.bytes[cursor.index] +_json_position(cursor::JSONLocationCursor) = + SourcePosition(cursor.line, cursor.column, cursor.index) + +function _json_advance!(cursor::JSONLocationCursor) + byte = _json_peek(cursor) + cursor.index += 1 + if byte == UInt8('\r') + cursor.line += 1 + cursor.column = 1 + cursor.after_cr = true + elseif byte == UInt8('\n') + cursor.after_cr || (cursor.line += 1) + cursor.column = 1 + cursor.after_cr = false + else + (byte & 0xc0) == 0x80 || (cursor.column += 1) + cursor.after_cr = false + end + return byte +end + +function _json_whitespace!(cursor::JSONLocationCursor) + while !_json_eof(cursor) && + _json_peek(cursor) in (UInt8(' '), UInt8('\t'), UInt8('\r'), UInt8('\n')) + _json_advance!(cursor) + end + return +end + +function _json_string!(cursor::JSONLocationCursor) + _json_advance!(cursor) == UInt8('"') || + throw(ArgumentError("internal JSON source-location scanner expected a string")) + while !_json_eof(cursor) + byte = _json_advance!(cursor) + byte == UInt8('"') && return + byte == UInt8('\\') || continue + _json_eof(cursor) && + throw(ArgumentError("internal JSON source-location scanner found an incomplete escape")) + escape = _json_advance!(cursor) + if escape == UInt8('u') + for _ in 1:4 + _json_eof(cursor) && throw( + ArgumentError( + "internal JSON source-location scanner found an incomplete Unicode escape", + ), + ) + _json_advance!(cursor) + end + end + end + throw(ArgumentError("internal JSON source-location scanner found an unterminated string")) +end + +function _json_scalar!(cursor::JSONLocationCursor) + if _json_peek(cursor) == UInt8('"') + _json_string!(cursor) + return + end + while !_json_eof(cursor) && + !(_json_peek(cursor) in ( + UInt8(' '), + UInt8('\t'), + UInt8('\r'), + UInt8('\n'), + UInt8(','), + UInt8(']'), + UInt8('}'), + )) + _json_advance!(cursor) + end + return +end + +function _json_value!( + cursor::JSONLocationCursor, + pointer::Resources.JSONPointer, + depth::Int, + max_depth::Int, +) + depth <= max_depth || + throw(ArgumentError("OpenAPI source exceeds the depth limit")) + _json_whitespace!(cursor) + _json_eof(cursor) && + throw(ArgumentError("internal JSON source-location scanner reached end of input")) + get!(cursor.locations, pointer, _json_position(cursor)) + byte = _json_peek(cursor) + if byte == UInt8('{') + _json_advance!(cursor) + _json_whitespace!(cursor) + if !_json_eof(cursor) && _json_peek(cursor) == UInt8('}') + _json_advance!(cursor) + return + end + while true + _json_whitespace!(cursor) + key_position = _json_position(cursor) + key_start = cursor.index + _json_string!(cursor) + key_end = cursor.index - 1 + key = JSON.parse( + String(copy(cursor.bytes[key_start:key_end])); + duplicate_keys = :error, + ) + child = pointer / key + cursor.locations[child] = key_position + _json_whitespace!(cursor) + _json_advance!(cursor) == UInt8(':') || throw( + ArgumentError("internal JSON source-location scanner expected ':'"), + ) + _json_value!(cursor, child, depth + 1, max_depth) + _json_whitespace!(cursor) + delimiter = _json_advance!(cursor) + delimiter == UInt8('}') && return + delimiter == UInt8(',') || throw( + ArgumentError("internal JSON source-location scanner expected ',' or '}'"), + ) + end + elseif byte == UInt8('[') + _json_advance!(cursor) + _json_whitespace!(cursor) + if !_json_eof(cursor) && _json_peek(cursor) == UInt8(']') + _json_advance!(cursor) + return + end + index = 0 + while true + _json_value!(cursor, pointer / string(index), depth + 1, max_depth) + index += 1 + _json_whitespace!(cursor) + delimiter = _json_advance!(cursor) + delimiter == UInt8(']') && return + delimiter == UInt8(',') || throw( + ArgumentError("internal JSON source-location scanner expected ',' or ']'"), + ) + end + end + _json_scalar!(cursor) + return +end + +function _json_locations(bytes::AbstractVector{UInt8}, max_depth::Int) + cursor = JSONLocationCursor(bytes) + _json_value!(cursor, Resources.JSONPointer(), 0, max_depth) + _json_whitespace!(cursor) + _json_eof(cursor) || throw( + ArgumentError("internal JSON source-location scanner found trailing input"), + ) + return cursor.locations +end + +function _line_starts(text::AbstractString) + starts = Int[firstindex(text)] + for index in eachindex(text) + text[index] == '\n' || continue + push!(starts, nextind(text, index)) + end + return starts +end + +function _byte_at_column( + text::AbstractString, + starts::Vector{Int}, + line::Int, + column::Int, +) + 1 <= line <= length(starts) || return ncodeunits(text) + 1 + index = starts[line] + stop = line < length(starts) ? starts[line + 1] - 1 : ncodeunits(text) + 1 + for _ in 1:column + index >= stop && return stop + index = nextind(text, index) + end + return index +end + +function _yaml_position(text, starts, mark) + line = Int(mark.line) + column = Int(mark.column) + return SourcePosition( + line, + column + 1, + _byte_at_column(text, starts, line, column), + ) +end + +function _yaml_locations!( + locations, + node, + pointer, + text, + starts, + active::IdDict{Any,Nothing}, +) + mark = getproperty(node, :start_mark) + mark === nothing || get!(locations, pointer, _yaml_position(text, starts, mark)) + node isa YAML.ScalarNode && return + haskey(active, node) && return + active[node] = nothing + try + if node isa YAML.MappingNode + for (key_node, value_node) in node.value + key_node isa YAML.ScalarNode || continue + child = pointer / String(key_node.value) + key_mark = key_node.start_mark + key_mark === nothing || + (locations[child] = _yaml_position(text, starts, key_mark)) + _yaml_locations!( + locations, + value_node, + child, + text, + starts, + active, + ) + end + elseif node isa YAML.SequenceNode + for (index, child_node) in enumerate(node.value) + _yaml_locations!( + locations, + child_node, + pointer / string(index - 1), + text, + starts, + active, + ) + end + end + finally + delete!(active, node) + end + return +end + +function _yaml_locations(text::AbstractString) + token_stream = YAML.TokenStream(IOBuffer(text)) + node = YAML.compose(YAML.EventStream(token_stream), YAML.Resolver()) + node isa YAML.MissingDocument && + return Dict{Resources.JSONPointer,SourcePosition}() + locations = Dict{Resources.JSONPointer,SourcePosition}() + _yaml_locations!( + locations, + node, + Resources.JSONPointer(), + text, + _line_starts(text), + IdDict{Any,Nothing}(), + ) + return locations +end + +function _position_at_byte(bytes::AbstractVector{UInt8}, byte::Integer) + target = clamp(Int(byte), 1, length(bytes) + 1) + cursor = JSONLocationCursor(bytes) + while cursor.index < target && !_json_eof(cursor) + _json_advance!(cursor) + end + return _json_position(cursor) +end + +function _parse_error_position(error, bytes::AbstractVector{UInt8}) + if error isa JSON.DuplicateKeyError + return _position_at_byte(bytes, error.position) + end + if hasproperty(error, :problem_mark) + mark = getproperty(error, :problem_mark) + if mark !== nothing + text = String(copy(bytes)) + isvalid(text) || return nothing + return _yaml_position(text, _line_starts(text), mark) + end + end + matched = match(r"byte position (\d+)", sprint(showerror, error)) + matched === nothing && return nothing + return _position_at_byte(bytes, parse(Int, matched.captures[1])) +end diff --git a/src/tools.jl b/src/tools.jl deleted file mode 100644 index f234dde..0000000 --- a/src/tools.jl +++ /dev/null @@ -1,311 +0,0 @@ -const SwaggerImage = ( - UI="swaggerapi/swagger-ui", - Editor="swaggerapi/swagger-editor", -) -const OpenAPIImage = ( - GeneratorOnline="openapitools/openapi-generator-online", - GeneratorCLI="openapitools/openapi-generator-cli", -) - -const GeneratorHost = ( - OpenAPIGeneratorTech = ( - Stable = "https://api.openapi-generator.tech", - Master = "https://api-latest-master.openapi-generator.tech", - ), - Local="http://localhost:8080", -) - -const GeneratorHeaders = [ - "Content-Type" => "application/json", - "Accept" => "application/json", -] - -docker_cmd(; use_sudo::Bool=false) = use_sudo ? `sudo docker` : `docker` - -function _start_docker(cmd, port) - run(cmd) - return "http://localhost:$port" -end - -function _stop_docker(image_name::AbstractString, image_type::AbstractString; use_sudo::Bool=false) - docker = docker_cmd(; use_sudo=use_sudo) - find_cmd = `$docker ps -a -q -f ancestor=$image_name` - container_id = strip(String(read(find_cmd))) - - if !isempty(container_id) - stop_cmd = `$docker stop $container_id` - stop_res = strip(String(read(stop_cmd))) - - if stop_res == container_id - @debug("Stopped $(image_type) container") - elseif isempty(stop_res) - @debug("$(image_type) container not running") - else - @error("Failed to stop $(image_type) container: $stop_res") - return false - end - - sleep(5) - container_id = strip(String(read(find_cmd))) - if !isempty(container_id) - rm_cmd = `$docker rm $container_id` - rm_res = strip(String(read(rm_cmd))) - - if rm_res == container_id - @debug("Removed $(image_type) container") - elseif isempty(rm_res) - @debug("$(image_type) container not found") - else - @error("Failed to remove $(image_type) container: $rm_res") - return false - end - end - - return true - else - @debug("$(image_type) container not found") - end - - return false -end - -""" - stop_openapi_generator(; use_sudo=false) - -Stop and remove the OpenAPI Generator container, if it is running. -Returns true if the container was stopped and removed, false otherwise. -""" -stop_openapi_generator(; use_sudo::Bool=false) = _stop_docker(OpenAPIImage.GeneratorOnline, "OpenAPI Generator"; use_sudo=use_sudo) - -""" - stop_swagger_ui(; use_sudo=false) - -Stop and remove the Swagger UI container, if it is running. -Returns true if the container was stopped and removed, false otherwise. -""" -stop_swagger_ui(; use_sudo::Bool=false) = _stop_swagger(SwaggerImage.UI; use_sudo=use_sudo) - -""" - stop_swagger_editor(; use_sudo=false) - -Stop and remove the Swagger Editor container, if it is running. -Returns true if the container was stopped and removed, false otherwise. -""" -stop_swagger_editor(; use_sudo::Bool=false) = _stop_swagger(SwaggerImage.Editor; use_sudo=use_sudo) - -""" - stop_swagger(; use_sudo=false) - -Stop and remove Swagger UI or Editor containers, if they are running. -Returns true if any container was stopped and removed, false otherwise. -""" -function stop_swagger(; use_sudo::Bool=false) - stopped = stop_swagger_ui(; use_sudo=use_sudo) - stopped |= stop_swagger_editor(; use_sudo=use_sudo) - return stopped -end - -_stop_swagger(image_name::AbstractString; use_sudo::Bool=false) = _stop_docker(image_name, "Swagger", use_sudo=use_sudo) -_start_swagger(cmd, port) = _start_docker(cmd, port) - -""" - openapi_generator(; port=8080, use_sudo=false) - -Start an OpenAPI Generator Online container. Returns the URL of the OpenAPI Generator. - -Optional arguments: -- `port`: The port to use for the OpenAPI Generator. Defaults to 8080. -- `use_sudo`: Whether to use `sudo` to run Docker commands. Defaults to false. -""" -function openapi_generator(; port::Int=8080, use_sudo::Bool=false) - docker = docker_cmd(; use_sudo=use_sudo) - cmd = `$docker run -d --rm -p $port:8080 $(OpenAPIImage.GeneratorOnline)` - return _start_docker(cmd, port) -end - -function _strip_trailing_pathsep(path::AbstractString) - if endswith(path, '/') - return path[1:end-1] - end - return path -end - -""" - generate( - spec::Dict{String,Any}; - type::Symbol=:client, - package_name::AbstractString="APIClient", - export_models::Bool=false, - export_operations::Bool=false, - output_dir::AbstractString="", - generator_host::AbstractString=GeneratorHost.Local - ) - -Generate client or server code from an OpenAPI spec using the OpenAPI Generator. -The OpenAPI Generator must be running at the specified `generator_host`. - -Returns the path to the generated code. - -Optional arguments: -- `type`: The type of code to generate. Must be `:client` or `:server`. Defaults to `:client`. -- `package_name`: The name of the package to generate. Defaults to "APIClient". -- `export_models`: Whether to export models. Defaults to false. -- `export_operations`: Whether to export operations. Defaults to false. -- `output_dir`: The directory to save the generated code. Defaults to a temporary directory. Directory will be created if it does not exist. -- `generator_host`: The host of the OpenAPI Generator. Defaults to `GeneratorHost.Local`. - Other possible values are `GeneratorHost.OpenAPIGeneratorTech.Stable` or `GeneratorHost.OpenAPIGeneratorTech.Master`, which point to - the service hosted by OpenAPI org. It can also be any other URL where the OpenAPI Generator is running. - -A locally hosted generator service is preferred by default for privacy reasons. -Use `openapi_generator` to start a local container. -Use `stop_openapi_generator` to stop the local generator service after use. -""" -function generate( - spec::Dict{String,Any}; - type::Symbol=:client, - package_name::AbstractString="APIClient", - export_models::Bool=false, - export_operations::Bool=false, - output_dir::AbstractString="", - generator_host::AbstractString=GeneratorHost.Local, -) - if type === :client - generator_path = "clients/julia-client" - elseif type === :server - generator_path = "servers/julia-server" - else - throw(ArgumentError("Invalid generator type: $type. Must be :client or :server")) - end - - if isempty(output_dir) - output_dir = mktempdir() - end - - url = _strip_trailing_pathsep(generator_host) * "/api/gen/" * generator_path - post_json = Dict{String,Any}( - "spec" => spec, - "options" => Dict{String,Any}( - "packageName" => package_name, - "exportModels" => string(export_models), - "exportOperations" => string(export_operations), - ) - ) - - out = PipeBuffer() - inp = PipeBuffer() - JSON.print(inp, post_json, 4) - closewrite(inp) - Downloads.request(url; method="POST", headers=GeneratorHeaders, input=inp, output=out, throw=true) - res = JSON.parse(out) - - url = res["link"] - mktempdir() do extracted_dir - mktempdir() do download_dir - output_file = joinpath(download_dir, "generated.zip") - open(output_file, "w") do out - Downloads.request(url; method="GET", output=out) - end - - p7zip = p7zip_jll.p7zip() - run(`$p7zip x -o$extracted_dir $output_file`) - - # we expect a single containing root directory in the extrated zip, the contents of which we move to the output directory - root_dir = only(readdir(extracted_dir)) - mkpath(output_dir) - for entry in readdir(joinpath(extracted_dir, root_dir)) - mv(joinpath(extracted_dir, root_dir, entry), joinpath(output_dir, entry); force=true) - end - end - end - - return output_dir -end - -""" - swagger_ui(spec; port=8080, use_sudo=false) - swagger_ui(spec_dir, spec_file; port=8080, use_sudo=false) - -Start a Swagger UI container for the given OpenAPI spec file. Returns the URL of the Swagger UI. - -Optional arguments: -- `port`: The port to use for the Swagger UI. Defaults to 8080. -- `use_sudo`: Whether to use `sudo` to run Docker commands. Defaults to false. -""" -function swagger_ui(spec::AbstractString; port::Int=8080, use_sudo::Bool=false) - spec = abspath(spec) - spec_dir = dirname(spec) - spec_file = basename(spec) - return swagger_ui(spec_dir, spec_file; port=port, use_sudo=use_sudo) -end - -function swagger_ui(spec_dir::AbstractString, spec_file::AbstractString; port::Int=8080, use_sudo::Bool=false) - docker = docker_cmd(; use_sudo=use_sudo) - cmd = `$docker run -d --rm -p $port:8080 -e SWAGGER_JSON=/spec/$spec_file -v $spec_dir:/spec $(SwaggerImage.UI)` - return _start_swagger(cmd, port) -end - -""" - swagger_editor(; port=8080, use_sudo=false) - swagger_editor(spec; port=8080, use_sudo=false) - swagger_editor(spec_dir, spec_file; port=8080, use_sudo=false) - -Start a Swagger Editor container with an optional OpenAPI spec file. Returns the URL of the Swagger Editor. - -Optional arguments: -- `port`: The port to use for the Swagger Editor. Defaults to 8080. -- `use_sudo`: Whether to use `sudo` to run Docker commands. Defaults to false. -""" -function swagger_editor(spec::AbstractString; port::Int=8080, use_sudo::Bool=false) - spec = abspath(spec) - spec_dir = dirname(spec) - spec_file = basename(spec) - return swagger_editor(spec_dir, spec_file; port=port, use_sudo=use_sudo) -end - -function swagger_editor(spec_dir::AbstractString, spec_file::AbstractString; port::Int=8080, use_sudo::Bool=false) - docker = docker_cmd(; use_sudo=use_sudo) - cmd = `$docker run -d --rm -p $port:8080 -e SWAGGER_FILE=/spec/$spec_file -v $spec_dir:/spec $(SwaggerImage.Editor)` - return _start_swagger(cmd, port) -end - -function swagger_editor(; port::Int=8080, use_sudo::Bool=false) - docker = docker_cmd(; use_sudo=use_sudo) - cmd = `$docker run -d --rm -p $port:8080 $(SwaggerImage.Editor)` - return _start_swagger(cmd, port) -end - -""" - lint(spec; use_sudo=false) - lint(spec_dir, spec_file; use_sudo=false) - -Lint an OpenAPI spec file using Spectral. - -Optional arguments: -- `use_sudo`: Whether to use `sudo` to run Docker commands. Defaults to false. -""" -function lint(spec::AbstractString; use_sudo::Bool=false) - spec = abspath(spec) - spec_dir = dirname(spec) - spec_file = basename(spec) - return lint(spec_dir, spec_file; use_sudo=use_sudo) -end - -function lint(spec_dir::AbstractString, spec_file::AbstractString; use_sudo::Bool=false) - docker = docker_cmd(; use_sudo=use_sudo) - if isfile(joinpath(spec_dir, ".spectral.yaml")) - @debug("linting with existing configuration") - cmd = `$docker run --rm -v $spec_dir:/spec:ro -w /spec stoplight/spectral:latest lint /spec/$spec_file` - run(cmd) - else - # generate a default configuration file - @debug("linting with default configuration") - mktempdir() do tmpdir - open(joinpath(tmpdir, ".spectral.yaml"), "w") do f - write(f, """extends: ["spectral:oas", "spectral:asyncapi"]""") - end - cp(joinpath(spec_dir, spec_file), joinpath(tmpdir, spec_file)) - cmd = `$docker run --rm -v $tmpdir:/spec:ro -w /spec stoplight/spectral:latest lint /spec/$spec_file` - run(cmd) - end - end -end \ No newline at end of file diff --git a/src/val.jl b/src/val.jl deleted file mode 100644 index c6cd73a..0000000 --- a/src/val.jl +++ /dev/null @@ -1,88 +0,0 @@ -val_max(val, lim, excl) = (excl ? (val < lim) : (val <= lim)) -val_min(val, lim, excl) = (excl ? (val > lim) : (val >= lim)) -val_max_length(val, lim) = (length(val) <= lim) -val_min_length(val, lim) = (length(val) >= lim) -val_enum(val, lst) = (val in lst) -function val_enum(val::Vector, lst) - for v in val - (v in lst) || return false - end - true -end -function val_enum(val::Dict, lst) - for v in keys(val) - (v in lst) || return false - end - true -end -function val_unique_items(val::Vector, is_unique) - is_unique || return true - return length(Set(val)) == length(val) -end -function val_pattern(val::AbstractString, pattern::Regex) - return !isnothing(match(pattern, val)) -end -val_format(val, format) = true # accept any unhandled format -val_format(val, format::AbstractString) = val_format(val, Val(Symbol(format))) -val_format(val::AbstractString, ::Val{:date}) = str2date(val) isa Date -val_format(val::AbstractString, ::Val{Symbol("date-time")}) = str2datetime(val) isa DateTime -val_format(val::AbstractString, ::Val{:byte}) = try - base64decode(val) - true -catch - false -end -val_format(val::Integer, ::Val{:int32}) = (typemin(Int32) <= val <= typemax(Int32)) -val_format(val::Integer, ::Val{:int64}) = (typemin(Int64) <= val <= typemax(Int64)) -val_format(val::AbstractFloat, ::Val{:float}) = (typemin(Float32) <= Float32(val) <= typemax(Float32)) -val_format(val::AbstractFloat, ::Val{:double}) = (typemin(Float64) <= Float64(val) <= typemax(Float64)) - -function val_multiple_of(val::Real, multiple_of::Real) - return isinteger(val / multiple_of) -end - -const MSG_INVALID_API_PARAM = Dict{Symbol,Function}([ - :maximum => (val,excl)->string("must be a value less than ", excl ? "or equal to " : "", val), - :minimum => (val,excl)->string("must be a value greater than ", excl ? "or equal to " : "", val), - :maxLength => (len)->string("length must be less than or equal to ", len), - :minLength => (len)->string("length must be greater than or equal to ", len), - :maxItems => (val)->string("number of items must be less than or equal to ", val), - :minItems => (val)->string("number of items must be greater than or equal to ", val), - :uniqueItems => (val)->string("items must be unique"), - :maxProperties => (val)->string("number of properties must be less than or equal to ", val), - :minProperties => (val)->string("number of properties must be greater than or equal to ", val), - :enum => (lst)->string("value is not from the allowed values ", lst), - :pattern => (val)->string("value does not match required pattern"), - :format => (val)->string("value does not match required format"), - :multipleOf => (val)->string("value must be a multiple of ", val), -]) - -const VAL_API_PARAM = Dict{Symbol,Function}([ - :maximum => val_max, - :minimum => val_min, - :maxLength => val_max_length, - :minLength => val_min_length, - :maxItems => val_max_length, - :minItems => val_min_length, - :uniqueItems => val_unique_items, - :maxProperties => val_max_length, - :minProperties => val_min_length, - :pattern => val_pattern, - :enum => val_enum, - :format => val_format, - :multipleOf => val_multiple_of, -]) - -function validate_param(parameter, operation_or_model, rule, value, args...) - # do not validate missing values - (value === nothing) && return - - VAL_API_PARAM[rule](value, args...) && return - - reason = string("Invalid value ($value) of parameter ", parameter, " for ", operation_or_model, ", ", MSG_INVALID_API_PARAM[rule](args...)) - throw(ValidationException(;reason, operation_or_model, value, parameter, rule, args)) -end - -validate_property(::Type{T}, name::Symbol, val) where {T<:APIModel} = nothing -validate_properties(::T) where {T<:APIModel} = nothing -check_required(::T) where {T<:APIModel} = true diff --git a/test/.gitignore b/test/.gitignore deleted file mode 100644 index c3f732c..0000000 --- a/test/.gitignore +++ /dev/null @@ -1 +0,0 @@ -openapi-generator-cli.jar diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 0000000..2f785c1 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,16 @@ +[deps] +Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +ZipFile = "a5390f91-8eb1-5f08-bee0-b1d1ffed6cea" + +[compat] +HTTP = "2" +JSON = "1.7" +ZipFile = "0.8, 0.9, 0.10" diff --git a/test/chunkreader_tests.jl b/test/chunkreader_tests.jl deleted file mode 100644 index fa2df4e..0000000 --- a/test/chunkreader_tests.jl +++ /dev/null @@ -1,352 +0,0 @@ -module ChunkReaderTests -using Test -using JSON -using OpenAPI -using OpenAPI.Clients: AbstractChunkReader, JSONChunkReader, LineChunkReader, RFC7464ChunkReader, _read_json_chunk - -function linechunk1() - buff = Base.BufferStream() - reader = LineChunkReader(buff) - results = String[] - readertask = @async begin - for line in reader - push!(results, String(line)) - end - end - write(buff, "hello\nworld\n") - write(buff, "goodbye\n") - close(buff) - wait(readertask) - @test results == ["hello", "world", "goodbye"] -end - -function linechunk2() - buff = Base.BufferStream() - reader = LineChunkReader(buff) - results = String[] - readertask = @async begin - for line in reader - push!(results, String(line)) - end - end - write(buff, "\nhello\nworld\n") - write(buff, "goodbye\n") - close(buff) - wait(readertask) - @test results == ["", "hello", "world", "goodbye"] -end - -function linechunk3() - buff = Base.BufferStream() - reader = LineChunkReader(buff) - results = String[] - readertask = @async begin - for line in reader - push!(results, String(line)) - end - end - write(buff, "hello\nworld\n") - write(buff, "goodbye") - close(buff) - wait(readertask) - @test results == ["hello", "world", "goodbye"] -end - -function jsonchunk1() - buff = Base.BufferStream() - reader = JSONChunkReader(buff) - results = String[] - readertask = @async begin - for json in reader - push!(results, String(json)) - end - end - - write(buff, "{\"hello\": \"world\"}") - write(buff, "{\"hello\": \"world\"}") - close(buff) - wait(readertask) - for result in results - json = JSON.parse(result) - @test json["hello"] == "world" - end - @test length(results) == 2 -end - -function jsonchunk2() - buff = Base.BufferStream() - reader = JSONChunkReader(buff) - results = String[] - readertask = @async begin - for json in reader - push!(results, String(json)) - end - end - - write(buff, "{\"hello\": \"world\"}\n") - write(buff, "{\"hello\": \"world\"}\n") - close(buff) - wait(readertask) - for result in results - json = JSON.parse(result) - @test json["hello"] == "world" - end - @test length(results) == 2 -end - -function jsonchunk3() - buff = Base.BufferStream() - reader = JSONChunkReader(buff) - results = String[] - readertask = @async begin - for json in reader - push!(results, String(json)) - end - end - - write(buff, "\n\n{\"hello\": \"world\"}\n\n") - write(buff, "{\"hello\": \"world\"}\n") - close(buff) - wait(readertask) - for result in results - json = JSON.parse(result) - @test json["hello"] == "world" - end - @test length(results) == 2 -end - -function jsonchunk4() - # A truncated trailing document (the stream closed mid-object, e.g. a dropped - # or cancelled streaming response) is discarded rather than handed to the JSON - # parser, so iteration ends cleanly instead of throwing "Unexpected end of - # input". The complete documents read before the truncation are still yielded. - buff = Base.BufferStream() - reader = JSONChunkReader(buff) - results = String[] - readertask = @async begin - for json in reader - push!(results, String(json)) - end - end - - write(buff, "\n\n{\"hello\": \"world\"}\n\n") - write(buff, "{\"hello\": \"world\"\n") # truncated: no closing brace before EOF - close(buff) - wait(readertask) # no throw - @test length(results) == 1 - @test JSON.parse(results[1])["hello"] == "world" -end - -function rfc7464chunk1() - buff = Base.BufferStream() - reader = RFC7464ChunkReader(buff) - results = String[] - readertask = @async begin - for chunk in reader - push!(results, String(chunk)) - end - end - - write(buff, OpenAPI.Clients.RFC7464_RECORD_SEPARATOR) - write(buff, "{\"hello\": \"world\"}") - write(buff, OpenAPI.Clients.RFC7464_RECORD_SEPARATOR) - write(buff, "{\"hello\": \"world\"}") - close(buff) - wait(readertask) - for result in results - if !isempty(result) - json = JSON.parse(result) - @test json["hello"] == "world" - end - end - @test length(results) == 3 -end - -function rfc7464chunk2() - buff = Base.BufferStream() - reader = RFC7464ChunkReader(buff) - results = String[] - readertask = @async begin - for chunk in reader - push!(results, String(chunk)) - end - end - - write(buff, "{\"hello\": \"world\"}") - write(buff, OpenAPI.Clients.RFC7464_RECORD_SEPARATOR) - write(buff, "{\"hello\": \"world\"}") - write(buff, OpenAPI.Clients.RFC7464_RECORD_SEPARATOR) - close(buff) - wait(readertask) - for result in results - if !isempty(result) - json = JSON.parse(result) - @test json["hello"] == "world" - end - end - @test length(results) == 2 -end - -function read_json_chunk_object() - io = IOBuffer("{\"key\": \"value\"}") - @test String(_read_json_chunk(io)) == "{\"key\": \"value\"}" - @test eof(io) -end - -function read_json_chunk_nested_object() - io = IOBuffer("{\"a\": {\"b\": 1}}") - @test String(_read_json_chunk(io)) == "{\"a\": {\"b\": 1}}" - @test eof(io) -end - -function read_json_chunk_array() - io = IOBuffer("[1, 2, 3]") - @test String(_read_json_chunk(io)) == "[1, 2, 3]" - @test eof(io) -end - -function read_json_chunk_nested_array() - io = IOBuffer("[[1,2],[3,4]]") - @test String(_read_json_chunk(io)) == "[[1,2],[3,4]]" - @test eof(io) -end - -function read_json_chunk_string() - io = IOBuffer("\"hello\"") - @test String(_read_json_chunk(io)) == "\"hello\"" - @test eof(io) -end - -function read_json_chunk_string_escaped_quote() - # embedded escaped quote: "say \"hi\"" - io = IOBuffer("\"say \\\"hi\\\"\"") - @test String(_read_json_chunk(io)) == "\"say \\\"hi\\\"\"" - @test eof(io) -end - -function read_json_chunk_string_escaped_backslash() - # embedded escaped backslash: "path\\file" - io = IOBuffer("\"path\\\\file\"") - @test String(_read_json_chunk(io)) == "\"path\\\\file\"" - @test eof(io) -end - -function read_json_chunk_integer() - io = IOBuffer("42") - @test String(_read_json_chunk(io)) == "42" - @test eof(io) -end - -function read_json_chunk_float() - io = IOBuffer("3.14") - @test String(_read_json_chunk(io)) == "3.14" - @test eof(io) -end - -function read_json_chunk_true() - io = IOBuffer("true") - @test String(_read_json_chunk(io)) == "true" - @test eof(io) -end - -function read_json_chunk_false() - io = IOBuffer("false") - @test String(_read_json_chunk(io)) == "false" - @test eof(io) -end - -function read_json_chunk_null() - io = IOBuffer("null") - @test String(_read_json_chunk(io)) == "null" - @test eof(io) -end - -function read_json_chunk_stops_at_boundary() - # reads exactly one chunk and leaves the stream positioned at the next value - io = IOBuffer("{\"a\":1}{\"b\":2}") - @test String(_read_json_chunk(io)) == "{\"a\":1}" - @test String(_read_json_chunk(io)) == "{\"b\":2}" - @test eof(io) -end - -function read_json_chunk_braces_in_string() - # braces inside a string value must not affect depth tracking - io = IOBuffer("{\"key\": \"value{nested}\"}") - @test String(_read_json_chunk(io)) == "{\"key\": \"value{nested}\"}" - @test eof(io) -end - -function read_json_chunk_brackets_in_string() - # brackets inside a string value must not affect depth tracking - io = IOBuffer("{\"key\": \"[not an array]\"}") - @test String(_read_json_chunk(io)) == "{\"key\": \"[not an array]\"}" - @test eof(io) -end - -function read_json_chunk_truncated_object() - # stream closed before the closing brace (e.g. dropped connection mid-object): - # the partial document is discarded rather than parsed. - io = IOBuffer("{\"statuses\":") - @test isempty(_read_json_chunk(io)) - @test eof(io) -end - -function read_json_chunk_truncated_nested_object() - io = IOBuffer("{\"a\": {\"b\": 1") - @test isempty(_read_json_chunk(io)) - @test eof(io) -end - -function read_json_chunk_truncated_array() - io = IOBuffer("[1, 2") - @test isempty(_read_json_chunk(io)) - @test eof(io) -end - -function read_json_chunk_truncated_string() - io = IOBuffer("\"dev/termina") - @test isempty(_read_json_chunk(io)) - @test eof(io) -end - -function read_json_chunk_complete_then_truncated() - # a complete document is returned; the following truncated one is discarded. - io = IOBuffer("{\"a\":1}{\"b\":") - @test String(_read_json_chunk(io)) == "{\"a\":1}" - @test isempty(_read_json_chunk(io)) - @test eof(io) -end - -function runtests() - linechunk1() - linechunk2() - linechunk3() - jsonchunk1() - jsonchunk2() - jsonchunk3() - jsonchunk4() - rfc7464chunk1() - rfc7464chunk2() - read_json_chunk_object() - read_json_chunk_nested_object() - read_json_chunk_array() - read_json_chunk_nested_array() - read_json_chunk_string() - read_json_chunk_string_escaped_quote() - read_json_chunk_string_escaped_backslash() - read_json_chunk_integer() - read_json_chunk_float() - read_json_chunk_true() - read_json_chunk_false() - read_json_chunk_null() - read_json_chunk_stops_at_boundary() - read_json_chunk_braces_in_string() - read_json_chunk_brackets_in_string() - read_json_chunk_truncated_object() - read_json_chunk_truncated_nested_object() - read_json_chunk_truncated_array() - read_json_chunk_truncated_string() - read_json_chunk_complete_then_truncated() -end - -end # module ChunkReaderTests \ No newline at end of file diff --git a/test/client/allany/AllAnyClient/.openapi-generator-ignore b/test/client/allany/AllAnyClient/.openapi-generator-ignore deleted file mode 100644 index 7484ee5..0000000 --- a/test/client/allany/AllAnyClient/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/test/client/allany/AllAnyClient/.openapi-generator/FILES b/test/client/allany/AllAnyClient/.openapi-generator/FILES deleted file mode 100644 index 47a1217..0000000 --- a/test/client/allany/AllAnyClient/.openapi-generator/FILES +++ /dev/null @@ -1,25 +0,0 @@ -README.md -docs/AnyOfBaseType.md -docs/AnyOfMappedPets.md -docs/AnyOfPets.md -docs/Cat.md -docs/DefaultApi.md -docs/Dog.md -docs/OneOfBaseType.md -docs/OneOfMappedPets.md -docs/OneOfPets.md -docs/Pet.md -docs/TypeWithAllArrayTypes.md -src/AllAnyClient.jl -src/apis/api_DefaultApi.jl -src/modelincludes.jl -src/models/model_AnyOfBaseType.jl -src/models/model_AnyOfMappedPets.jl -src/models/model_AnyOfPets.jl -src/models/model_Cat.jl -src/models/model_Dog.jl -src/models/model_OneOfBaseType.jl -src/models/model_OneOfMappedPets.jl -src/models/model_OneOfPets.jl -src/models/model_Pet.jl -src/models/model_TypeWithAllArrayTypes.jl diff --git a/test/client/allany/AllAnyClient/.openapi-generator/VERSION b/test/client/allany/AllAnyClient/.openapi-generator/VERSION deleted file mode 100644 index 96cfbb1..0000000 --- a/test/client/allany/AllAnyClient/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.13.0-SNAPSHOT diff --git a/test/client/allany/AllAnyClient/README.md b/test/client/allany/AllAnyClient/README.md deleted file mode 100644 index 97fc884..0000000 --- a/test/client/allany/AllAnyClient/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Julia API client for AllAnyClient - -API to test code generation for oneof anyof allof - -## Overview -This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. - -- API version: 0.0.1 -- Generator version: 7.13.0-SNAPSHOT -- Build package: org.openapitools.codegen.languages.JuliaClientCodegen - - -## Installation -Place the Julia files generated under the `src` folder in your Julia project. Include AllAnyClient.jl in the project code. -It would include the module named AllAnyClient. - -Documentation is generated as markdown files under the `docs` folder. You can include them in your project documentation. -Documentation is also embedded in Julia which can be used with a Julia specific documentation generator. - -## API Endpoints - -Class | Method ------------- | ------------- -*DefaultApi* | [**echo_anyof_base_type_post**](docs/DefaultApi.md#echo_anyof_base_type_post)
**POST** /echo_anyof_base_type
-*DefaultApi* | [**echo_anyof_mapped_pets_post**](docs/DefaultApi.md#echo_anyof_mapped_pets_post)
**POST** /echo_anyof_mapped_pets
-*DefaultApi* | [**echo_anyof_pets_post**](docs/DefaultApi.md#echo_anyof_pets_post)
**POST** /echo_anyof_pets
-*DefaultApi* | [**echo_arrays_post**](docs/DefaultApi.md#echo_arrays_post)
**POST** /echo_arrays
-*DefaultApi* | [**echo_oneof_base_type_post**](docs/DefaultApi.md#echo_oneof_base_type_post)
**POST** /echo_oneof_base_type
-*DefaultApi* | [**echo_oneof_mapped_pets_post**](docs/DefaultApi.md#echo_oneof_mapped_pets_post)
**POST** /echo_oneof_mapped_pets
-*DefaultApi* | [**echo_oneof_pets_post**](docs/DefaultApi.md#echo_oneof_pets_post)
**POST** /echo_oneof_pets
- - -## Models - - - [AnyOfBaseType](docs/AnyOfBaseType.md) - - [AnyOfMappedPets](docs/AnyOfMappedPets.md) - - [AnyOfPets](docs/AnyOfPets.md) - - [Cat](docs/Cat.md) - - [Dog](docs/Dog.md) - - [OneOfBaseType](docs/OneOfBaseType.md) - - [OneOfMappedPets](docs/OneOfMappedPets.md) - - [OneOfPets](docs/OneOfPets.md) - - [Pet](docs/Pet.md) - - [TypeWithAllArrayTypes](docs/TypeWithAllArrayTypes.md) - - - -## Authorization -Endpoints do not require authorization. - - -## Author - -test@example.com - diff --git a/test/client/allany/AllAnyClient/docs/AnyOfBaseType.md b/test/client/allany/AllAnyClient/docs/AnyOfBaseType.md deleted file mode 100644 index f4e20a9..0000000 --- a/test/client/allany/AllAnyClient/docs/AnyOfBaseType.md +++ /dev/null @@ -1,16 +0,0 @@ -# AnyOfBaseType - - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**value** | This is a anyOf model. The value must be any of the following types: Float64, String | | [optional] - - - - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/allany/AllAnyClient/docs/AnyOfMappedPets.md b/test/client/allany/AllAnyClient/docs/AnyOfMappedPets.md deleted file mode 100644 index de0a75e..0000000 --- a/test/client/allany/AllAnyClient/docs/AnyOfMappedPets.md +++ /dev/null @@ -1,19 +0,0 @@ -# AnyOfMappedPets - - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**value** | This is a anyOf model. The value must be any of the following types: Cat, Dog | | [optional] - -The discriminator field is `pet_type` with the following mapping: - - `cat`: `Cat` - - `dog`: `Dog` - - - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/allany/AllAnyClient/docs/AnyOfPets.md b/test/client/allany/AllAnyClient/docs/AnyOfPets.md deleted file mode 100644 index 3718118..0000000 --- a/test/client/allany/AllAnyClient/docs/AnyOfPets.md +++ /dev/null @@ -1,16 +0,0 @@ -# AnyOfPets - - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**value** | This is a anyOf model. The value must be any of the following types: Cat, Dog | | [optional] - -The discriminator field is `pet_type` - - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/allany/AllAnyClient/docs/Cat.md b/test/client/allany/AllAnyClient/docs/Cat.md deleted file mode 100644 index 606bbcf..0000000 --- a/test/client/allany/AllAnyClient/docs/Cat.md +++ /dev/null @@ -1,14 +0,0 @@ -# Cat - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**pet_type** | **String** | | [default to nothing] -**hunts** | **Bool** | | [optional] [default to nothing] -**age** | **Int64** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/allany/AllAnyClient/docs/CatAllOf.md b/test/client/allany/AllAnyClient/docs/CatAllOf.md deleted file mode 100644 index 3f72abe..0000000 --- a/test/client/allany/AllAnyClient/docs/CatAllOf.md +++ /dev/null @@ -1,13 +0,0 @@ -# CatAllOf - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**hunts** | **Bool** | | [optional] [default to nothing] -**age** | **Int64** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/allany/AllAnyClient/docs/DefaultApi.md b/test/client/allany/AllAnyClient/docs/DefaultApi.md deleted file mode 100644 index 0d8ecc0..0000000 --- a/test/client/allany/AllAnyClient/docs/DefaultApi.md +++ /dev/null @@ -1,211 +0,0 @@ -# DefaultApi - -All URIs are relative to *http://localhost* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**echo_anyof_base_type_post**](DefaultApi.md#echo_anyof_base_type_post) | **POST** /echo_anyof_base_type | -[**echo_anyof_mapped_pets_post**](DefaultApi.md#echo_anyof_mapped_pets_post) | **POST** /echo_anyof_mapped_pets | -[**echo_anyof_pets_post**](DefaultApi.md#echo_anyof_pets_post) | **POST** /echo_anyof_pets | -[**echo_arrays_post**](DefaultApi.md#echo_arrays_post) | **POST** /echo_arrays | -[**echo_oneof_base_type_post**](DefaultApi.md#echo_oneof_base_type_post) | **POST** /echo_oneof_base_type | -[**echo_oneof_mapped_pets_post**](DefaultApi.md#echo_oneof_mapped_pets_post) | **POST** /echo_oneof_mapped_pets | -[**echo_oneof_pets_post**](DefaultApi.md#echo_oneof_pets_post) | **POST** /echo_oneof_pets | - - -# **echo_anyof_base_type_post** -> echo_anyof_base_type_post(_api::DefaultApi, any_of_base_type::AnyOfBaseType; _mediaType=nothing) -> AnyOfBaseType, OpenAPI.Clients.ApiResponse
-> echo_anyof_base_type_post(_api::DefaultApi, response_stream::Channel, any_of_base_type::AnyOfBaseType; _mediaType=nothing) -> Channel{ AnyOfBaseType }, OpenAPI.Clients.ApiResponse - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **DefaultApi** | API context | -**any_of_base_type** | [**AnyOfBaseType**](AnyOfBaseType.md) | | - -### Return type - -[**AnyOfBaseType**](AnyOfBaseType.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **echo_anyof_mapped_pets_post** -> echo_anyof_mapped_pets_post(_api::DefaultApi, any_of_mapped_pets::AnyOfMappedPets; _mediaType=nothing) -> AnyOfMappedPets, OpenAPI.Clients.ApiResponse
-> echo_anyof_mapped_pets_post(_api::DefaultApi, response_stream::Channel, any_of_mapped_pets::AnyOfMappedPets; _mediaType=nothing) -> Channel{ AnyOfMappedPets }, OpenAPI.Clients.ApiResponse - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **DefaultApi** | API context | -**any_of_mapped_pets** | [**AnyOfMappedPets**](AnyOfMappedPets.md) | | - -### Return type - -[**AnyOfMappedPets**](AnyOfMappedPets.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **echo_anyof_pets_post** -> echo_anyof_pets_post(_api::DefaultApi, any_of_pets::AnyOfPets; _mediaType=nothing) -> AnyOfPets, OpenAPI.Clients.ApiResponse
-> echo_anyof_pets_post(_api::DefaultApi, response_stream::Channel, any_of_pets::AnyOfPets; _mediaType=nothing) -> Channel{ AnyOfPets }, OpenAPI.Clients.ApiResponse - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **DefaultApi** | API context | -**any_of_pets** | [**AnyOfPets**](AnyOfPets.md) | | - -### Return type - -[**AnyOfPets**](AnyOfPets.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **echo_arrays_post** -> echo_arrays_post(_api::DefaultApi, type_with_all_array_types::TypeWithAllArrayTypes; _mediaType=nothing) -> TypeWithAllArrayTypes, OpenAPI.Clients.ApiResponse
-> echo_arrays_post(_api::DefaultApi, response_stream::Channel, type_with_all_array_types::TypeWithAllArrayTypes; _mediaType=nothing) -> Channel{ TypeWithAllArrayTypes }, OpenAPI.Clients.ApiResponse - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **DefaultApi** | API context | -**type_with_all_array_types** | [**TypeWithAllArrayTypes**](TypeWithAllArrayTypes.md) | | - -### Return type - -[**TypeWithAllArrayTypes**](TypeWithAllArrayTypes.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **echo_oneof_base_type_post** -> echo_oneof_base_type_post(_api::DefaultApi, one_of_base_type::OneOfBaseType; _mediaType=nothing) -> OneOfBaseType, OpenAPI.Clients.ApiResponse
-> echo_oneof_base_type_post(_api::DefaultApi, response_stream::Channel, one_of_base_type::OneOfBaseType; _mediaType=nothing) -> Channel{ OneOfBaseType }, OpenAPI.Clients.ApiResponse - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **DefaultApi** | API context | -**one_of_base_type** | [**OneOfBaseType**](OneOfBaseType.md) | | - -### Return type - -[**OneOfBaseType**](OneOfBaseType.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **echo_oneof_mapped_pets_post** -> echo_oneof_mapped_pets_post(_api::DefaultApi, one_of_mapped_pets::OneOfMappedPets; _mediaType=nothing) -> OneOfMappedPets, OpenAPI.Clients.ApiResponse
-> echo_oneof_mapped_pets_post(_api::DefaultApi, response_stream::Channel, one_of_mapped_pets::OneOfMappedPets; _mediaType=nothing) -> Channel{ OneOfMappedPets }, OpenAPI.Clients.ApiResponse - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **DefaultApi** | API context | -**one_of_mapped_pets** | [**OneOfMappedPets**](OneOfMappedPets.md) | | - -### Return type - -[**OneOfMappedPets**](OneOfMappedPets.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **echo_oneof_pets_post** -> echo_oneof_pets_post(_api::DefaultApi, one_of_pets::OneOfPets; _mediaType=nothing) -> OneOfPets, OpenAPI.Clients.ApiResponse
-> echo_oneof_pets_post(_api::DefaultApi, response_stream::Channel, one_of_pets::OneOfPets; _mediaType=nothing) -> Channel{ OneOfPets }, OpenAPI.Clients.ApiResponse - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **DefaultApi** | API context | -**one_of_pets** | [**OneOfPets**](OneOfPets.md) | | - -### Return type - -[**OneOfPets**](OneOfPets.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - diff --git a/test/client/allany/AllAnyClient/docs/Dog.md b/test/client/allany/AllAnyClient/docs/Dog.md deleted file mode 100644 index 6f348dc..0000000 --- a/test/client/allany/AllAnyClient/docs/Dog.md +++ /dev/null @@ -1,14 +0,0 @@ -# Dog - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**pet_type** | **String** | | [default to nothing] -**bark** | **Bool** | | [optional] [default to nothing] -**breed** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/allany/AllAnyClient/docs/DogAllOf.md b/test/client/allany/AllAnyClient/docs/DogAllOf.md deleted file mode 100644 index 28333b9..0000000 --- a/test/client/allany/AllAnyClient/docs/DogAllOf.md +++ /dev/null @@ -1,13 +0,0 @@ -# DogAllOf - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**bark** | **Bool** | | [optional] [default to nothing] -**breed** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/allany/AllAnyClient/docs/OneOfBaseType.md b/test/client/allany/AllAnyClient/docs/OneOfBaseType.md deleted file mode 100644 index 2347884..0000000 --- a/test/client/allany/AllAnyClient/docs/OneOfBaseType.md +++ /dev/null @@ -1,15 +0,0 @@ -# OneOfBaseType - - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**value** | This is a oneOf model. The value must be exactly one of the following types: Float64, String | | [optional] - - - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/allany/AllAnyClient/docs/OneOfMappedPets.md b/test/client/allany/AllAnyClient/docs/OneOfMappedPets.md deleted file mode 100644 index 8b329e0..0000000 --- a/test/client/allany/AllAnyClient/docs/OneOfMappedPets.md +++ /dev/null @@ -1,18 +0,0 @@ -# OneOfMappedPets - - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**value** | This is a oneOf model. The value must be exactly one of the following types: Cat, Dog | | [optional] - -The discriminator field is `pet_type` with the following mapping: - - `cat`: `Cat` - - `dog`: `Dog` - - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/allany/AllAnyClient/docs/OneOfPets.md b/test/client/allany/AllAnyClient/docs/OneOfPets.md deleted file mode 100644 index 3b2a95f..0000000 --- a/test/client/allany/AllAnyClient/docs/OneOfPets.md +++ /dev/null @@ -1,15 +0,0 @@ -# OneOfPets - - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**value** | This is a oneOf model. The value must be exactly one of the following types: Cat, Dog | | [optional] - -The discriminator field is `pet_type` - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/allany/AllAnyClient/docs/Pet.md b/test/client/allany/AllAnyClient/docs/Pet.md deleted file mode 100644 index 2b7fbbb..0000000 --- a/test/client/allany/AllAnyClient/docs/Pet.md +++ /dev/null @@ -1,12 +0,0 @@ -# Pet - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**pet_type** | **String** | | [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/allany/AllAnyClient/docs/TypeWithAllArrayTypes.md b/test/client/allany/AllAnyClient/docs/TypeWithAllArrayTypes.md deleted file mode 100644 index e3f3acf..0000000 --- a/test/client/allany/AllAnyClient/docs/TypeWithAllArrayTypes.md +++ /dev/null @@ -1,15 +0,0 @@ -# TypeWithAllArrayTypes - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**oneofbase** | [**Vector{OneOfBaseType}**](OneOfBaseType.md) | | [optional] [default to nothing] -**anyofbase** | [**Vector{AnyOfBaseType}**](AnyOfBaseType.md) | | [optional] [default to nothing] -**oneofpets** | [**Vector{OneOfPets}**](OneOfPets.md) | | [optional] [default to nothing] -**anyofpets** | [**Vector{AnyOfPets}**](AnyOfPets.md) | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/allany/AllAnyClient/src/AllAnyClient.jl b/test/client/allany/AllAnyClient/src/AllAnyClient.jl deleted file mode 100644 index d683cce..0000000 --- a/test/client/allany/AllAnyClient/src/AllAnyClient.jl +++ /dev/null @@ -1,16 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -module AllAnyClient - -using Dates, TimeZones -using OpenAPI -using OpenAPI.Clients - -const API_VERSION = "0.0.1" - -include("modelincludes.jl") - -include("apis/api_DefaultApi.jl") - -end # module AllAnyClient diff --git a/test/client/allany/AllAnyClient/src/apis/api_DefaultApi.jl b/test/client/allany/AllAnyClient/src/apis/api_DefaultApi.jl deleted file mode 100644 index 72139a5..0000000 --- a/test/client/allany/AllAnyClient/src/apis/api_DefaultApi.jl +++ /dev/null @@ -1,202 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -struct DefaultApi <: OpenAPI.APIClientImpl - client::OpenAPI.Clients.Client -end - -""" -The default API base path for APIs in `DefaultApi`. -This can be used to construct the `OpenAPI.Clients.Client` instance. -""" -basepath(::Type{ DefaultApi }) = "http://localhost" - -const _returntypes_echo_anyof_base_type_post_DefaultApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => AnyOfBaseType, -) - -function _oacinternal_echo_anyof_base_type_post(_api::DefaultApi, any_of_base_type::AnyOfBaseType; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_echo_anyof_base_type_post_DefaultApi, "/echo_anyof_base_type", [], any_of_base_type) - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Params: -- any_of_base_type::AnyOfBaseType (required) - -Return: AnyOfBaseType, OpenAPI.Clients.ApiResponse -""" -function echo_anyof_base_type_post(_api::DefaultApi, any_of_base_type::AnyOfBaseType; _mediaType=nothing) - _ctx = _oacinternal_echo_anyof_base_type_post(_api, any_of_base_type; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function echo_anyof_base_type_post(_api::DefaultApi, response_stream::Channel, any_of_base_type::AnyOfBaseType; _mediaType=nothing) - _ctx = _oacinternal_echo_anyof_base_type_post(_api, any_of_base_type; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_echo_anyof_mapped_pets_post_DefaultApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => AnyOfMappedPets, -) - -function _oacinternal_echo_anyof_mapped_pets_post(_api::DefaultApi, any_of_mapped_pets::AnyOfMappedPets; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_echo_anyof_mapped_pets_post_DefaultApi, "/echo_anyof_mapped_pets", [], any_of_mapped_pets) - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Params: -- any_of_mapped_pets::AnyOfMappedPets (required) - -Return: AnyOfMappedPets, OpenAPI.Clients.ApiResponse -""" -function echo_anyof_mapped_pets_post(_api::DefaultApi, any_of_mapped_pets::AnyOfMappedPets; _mediaType=nothing) - _ctx = _oacinternal_echo_anyof_mapped_pets_post(_api, any_of_mapped_pets; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function echo_anyof_mapped_pets_post(_api::DefaultApi, response_stream::Channel, any_of_mapped_pets::AnyOfMappedPets; _mediaType=nothing) - _ctx = _oacinternal_echo_anyof_mapped_pets_post(_api, any_of_mapped_pets; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_echo_anyof_pets_post_DefaultApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => AnyOfPets, -) - -function _oacinternal_echo_anyof_pets_post(_api::DefaultApi, any_of_pets::AnyOfPets; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_echo_anyof_pets_post_DefaultApi, "/echo_anyof_pets", [], any_of_pets) - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Params: -- any_of_pets::AnyOfPets (required) - -Return: AnyOfPets, OpenAPI.Clients.ApiResponse -""" -function echo_anyof_pets_post(_api::DefaultApi, any_of_pets::AnyOfPets; _mediaType=nothing) - _ctx = _oacinternal_echo_anyof_pets_post(_api, any_of_pets; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function echo_anyof_pets_post(_api::DefaultApi, response_stream::Channel, any_of_pets::AnyOfPets; _mediaType=nothing) - _ctx = _oacinternal_echo_anyof_pets_post(_api, any_of_pets; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_echo_arrays_post_DefaultApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => TypeWithAllArrayTypes, -) - -function _oacinternal_echo_arrays_post(_api::DefaultApi, type_with_all_array_types::TypeWithAllArrayTypes; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_echo_arrays_post_DefaultApi, "/echo_arrays", [], type_with_all_array_types) - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Params: -- type_with_all_array_types::TypeWithAllArrayTypes (required) - -Return: TypeWithAllArrayTypes, OpenAPI.Clients.ApiResponse -""" -function echo_arrays_post(_api::DefaultApi, type_with_all_array_types::TypeWithAllArrayTypes; _mediaType=nothing) - _ctx = _oacinternal_echo_arrays_post(_api, type_with_all_array_types; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function echo_arrays_post(_api::DefaultApi, response_stream::Channel, type_with_all_array_types::TypeWithAllArrayTypes; _mediaType=nothing) - _ctx = _oacinternal_echo_arrays_post(_api, type_with_all_array_types; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_echo_oneof_base_type_post_DefaultApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => OneOfBaseType, -) - -function _oacinternal_echo_oneof_base_type_post(_api::DefaultApi, one_of_base_type::OneOfBaseType; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_echo_oneof_base_type_post_DefaultApi, "/echo_oneof_base_type", [], one_of_base_type) - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Params: -- one_of_base_type::OneOfBaseType (required) - -Return: OneOfBaseType, OpenAPI.Clients.ApiResponse -""" -function echo_oneof_base_type_post(_api::DefaultApi, one_of_base_type::OneOfBaseType; _mediaType=nothing) - _ctx = _oacinternal_echo_oneof_base_type_post(_api, one_of_base_type; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function echo_oneof_base_type_post(_api::DefaultApi, response_stream::Channel, one_of_base_type::OneOfBaseType; _mediaType=nothing) - _ctx = _oacinternal_echo_oneof_base_type_post(_api, one_of_base_type; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_echo_oneof_mapped_pets_post_DefaultApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => OneOfMappedPets, -) - -function _oacinternal_echo_oneof_mapped_pets_post(_api::DefaultApi, one_of_mapped_pets::OneOfMappedPets; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_echo_oneof_mapped_pets_post_DefaultApi, "/echo_oneof_mapped_pets", [], one_of_mapped_pets) - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Params: -- one_of_mapped_pets::OneOfMappedPets (required) - -Return: OneOfMappedPets, OpenAPI.Clients.ApiResponse -""" -function echo_oneof_mapped_pets_post(_api::DefaultApi, one_of_mapped_pets::OneOfMappedPets; _mediaType=nothing) - _ctx = _oacinternal_echo_oneof_mapped_pets_post(_api, one_of_mapped_pets; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function echo_oneof_mapped_pets_post(_api::DefaultApi, response_stream::Channel, one_of_mapped_pets::OneOfMappedPets; _mediaType=nothing) - _ctx = _oacinternal_echo_oneof_mapped_pets_post(_api, one_of_mapped_pets; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_echo_oneof_pets_post_DefaultApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => OneOfPets, -) - -function _oacinternal_echo_oneof_pets_post(_api::DefaultApi, one_of_pets::OneOfPets; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_echo_oneof_pets_post_DefaultApi, "/echo_oneof_pets", [], one_of_pets) - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Params: -- one_of_pets::OneOfPets (required) - -Return: OneOfPets, OpenAPI.Clients.ApiResponse -""" -function echo_oneof_pets_post(_api::DefaultApi, one_of_pets::OneOfPets; _mediaType=nothing) - _ctx = _oacinternal_echo_oneof_pets_post(_api, one_of_pets; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function echo_oneof_pets_post(_api::DefaultApi, response_stream::Channel, one_of_pets::OneOfPets; _mediaType=nothing) - _ctx = _oacinternal_echo_oneof_pets_post(_api, one_of_pets; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -export echo_anyof_base_type_post -export echo_anyof_mapped_pets_post -export echo_anyof_pets_post -export echo_arrays_post -export echo_oneof_base_type_post -export echo_oneof_mapped_pets_post -export echo_oneof_pets_post diff --git a/test/client/allany/AllAnyClient/src/modelincludes.jl b/test/client/allany/AllAnyClient/src/modelincludes.jl deleted file mode 100644 index fe46dd5..0000000 --- a/test/client/allany/AllAnyClient/src/modelincludes.jl +++ /dev/null @@ -1,13 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -include("models/model_AnyOfBaseType.jl") -include("models/model_AnyOfMappedPets.jl") -include("models/model_AnyOfPets.jl") -include("models/model_Cat.jl") -include("models/model_Dog.jl") -include("models/model_OneOfBaseType.jl") -include("models/model_OneOfMappedPets.jl") -include("models/model_OneOfPets.jl") -include("models/model_Pet.jl") -include("models/model_TypeWithAllArrayTypes.jl") diff --git a/test/client/allany/AllAnyClient/src/models/model_AnyOfBaseType.jl b/test/client/allany/AllAnyClient/src/models/model_AnyOfBaseType.jl deleted file mode 100644 index 57a265c..0000000 --- a/test/client/allany/AllAnyClient/src/models/model_AnyOfBaseType.jl +++ /dev/null @@ -1,20 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - - -@doc raw"""AnyOfBaseType - - AnyOfBaseType(; value=nothing) -""" -mutable struct AnyOfBaseType <: OpenAPI.AnyOfAPIModel - value::Any # Union{ Float64, String } - AnyOfBaseType() = new() - AnyOfBaseType(value) = new(value) -end # type AnyOfBaseType - -function OpenAPI.property_type(::Type{ AnyOfBaseType }, name::Symbol, json::Dict{String,Any}) - - # no discriminator specified, can't determine the exact type - return fieldtype(AnyOfBaseType, name) -end diff --git a/test/client/allany/AllAnyClient/src/models/model_AnyOfMappedPets.jl b/test/client/allany/AllAnyClient/src/models/model_AnyOfMappedPets.jl deleted file mode 100644 index 9522b4f..0000000 --- a/test/client/allany/AllAnyClient/src/models/model_AnyOfMappedPets.jl +++ /dev/null @@ -1,24 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - - -@doc raw"""AnyOfMappedPets - - AnyOfMappedPets(; value=nothing) -""" -mutable struct AnyOfMappedPets <: OpenAPI.AnyOfAPIModel - value::Any # Union{ Cat, Dog } - AnyOfMappedPets() = new() - AnyOfMappedPets(value) = new(value) -end # type AnyOfMappedPets - -function OpenAPI.property_type(::Type{ AnyOfMappedPets }, name::Symbol, json::Dict{String,Any}) - discriminator = json["pet_type"] - if discriminator == "cat" - return eval(Base.Meta.parse("Cat")) - elseif discriminator == "dog" - return eval(Base.Meta.parse("Dog")) - end - throw(OpenAPI.ValidationException("Invalid discriminator value: $discriminator for AnyOfMappedPets")) -end diff --git a/test/client/allany/AllAnyClient/src/models/model_AnyOfPets.jl b/test/client/allany/AllAnyClient/src/models/model_AnyOfPets.jl deleted file mode 100644 index fa158d7..0000000 --- a/test/client/allany/AllAnyClient/src/models/model_AnyOfPets.jl +++ /dev/null @@ -1,24 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - - -@doc raw"""AnyOfPets - - AnyOfPets(; value=nothing) -""" -mutable struct AnyOfPets <: OpenAPI.AnyOfAPIModel - value::Any # Union{ Cat, Dog } - AnyOfPets() = new() - AnyOfPets(value) = new(value) -end # type AnyOfPets - -function OpenAPI.property_type(::Type{ AnyOfPets }, name::Symbol, json::Dict{String,Any}) - discriminator = json["pet_type"] - if discriminator == "Cat" - return eval(Base.Meta.parse("Cat")) - elseif discriminator == "Dog" - return eval(Base.Meta.parse("Dog")) - end - throw(OpenAPI.ValidationException("Invalid discriminator value: $discriminator for AnyOfPets")) -end diff --git a/test/client/allany/AllAnyClient/src/models/model_Cat.jl b/test/client/allany/AllAnyClient/src/models/model_Cat.jl deleted file mode 100644 index 1f720cf..0000000 --- a/test/client/allany/AllAnyClient/src/models/model_Cat.jl +++ /dev/null @@ -1,47 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Cat - - Cat(; - pet_type=nothing, - hunts=nothing, - age=nothing, - ) - - - pet_type::String - - hunts::Bool - - age::Int64 -""" -Base.@kwdef mutable struct Cat <: OpenAPI.APIModel - pet_type::Union{Nothing, String} = nothing - hunts::Union{Nothing, Bool} = nothing - age::Union{Nothing, Int64} = nothing - - function Cat(pet_type, hunts, age, ) - o = new(pet_type, hunts, age, ) - OpenAPI.validate_properties(o) - return o - end -end # type Cat - -const _property_types_Cat = Dict{Symbol,String}(Symbol("pet_type")=>"String", Symbol("hunts")=>"Bool", Symbol("age")=>"Int64", ) -OpenAPI.property_type(::Type{ Cat }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Cat[name]))} - -function OpenAPI.check_required(o::Cat) - o.pet_type === nothing && (return false) - true -end - -function OpenAPI.validate_properties(o::Cat) - OpenAPI.validate_property(Cat, Symbol("pet_type"), o.pet_type) - OpenAPI.validate_property(Cat, Symbol("hunts"), o.hunts) - OpenAPI.validate_property(Cat, Symbol("age"), o.age) -end - -function OpenAPI.validate_property(::Type{ Cat }, name::Symbol, val) - - - -end diff --git a/test/client/allany/AllAnyClient/src/models/model_CatAllOf.jl b/test/client/allany/AllAnyClient/src/models/model_CatAllOf.jl deleted file mode 100644 index c79f430..0000000 --- a/test/client/allany/AllAnyClient/src/models/model_CatAllOf.jl +++ /dev/null @@ -1,35 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Cat_allOf - - CatAllOf(; - hunts=nothing, - age=nothing, - ) - - - hunts::Bool - - age::Int64 -""" -Base.@kwdef mutable struct CatAllOf <: OpenAPI.APIModel - hunts::Union{Nothing, Bool} = nothing - age::Union{Nothing, Int64} = nothing - - function CatAllOf(hunts, age, ) - OpenAPI.validate_property(CatAllOf, Symbol("hunts"), hunts) - OpenAPI.validate_property(CatAllOf, Symbol("age"), age) - return new(hunts, age, ) - end -end # type CatAllOf - -const _property_types_CatAllOf = Dict{Symbol,String}(Symbol("hunts")=>"Bool", Symbol("age")=>"Int64", ) -OpenAPI.property_type(::Type{ CatAllOf }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_CatAllOf[name]))} - -function check_required(o::CatAllOf) - true -end - -function OpenAPI.validate_property(::Type{ CatAllOf }, name::Symbol, val) -end - diff --git a/test/client/allany/AllAnyClient/src/models/model_Dog.jl b/test/client/allany/AllAnyClient/src/models/model_Dog.jl deleted file mode 100644 index c5ac6b9..0000000 --- a/test/client/allany/AllAnyClient/src/models/model_Dog.jl +++ /dev/null @@ -1,51 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Dog - - Dog(; - pet_type=nothing, - bark=nothing, - breed=nothing, - ) - - - pet_type::String - - bark::Bool - - breed::String -""" -Base.@kwdef mutable struct Dog <: OpenAPI.APIModel - pet_type::Union{Nothing, String} = nothing - bark::Union{Nothing, Bool} = nothing - breed::Union{Nothing, String} = nothing - - function Dog(pet_type, bark, breed, ) - o = new(pet_type, bark, breed, ) - OpenAPI.validate_properties(o) - return o - end -end # type Dog - -const _property_types_Dog = Dict{Symbol,String}(Symbol("pet_type")=>"String", Symbol("bark")=>"Bool", Symbol("breed")=>"String", ) -OpenAPI.property_type(::Type{ Dog }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Dog[name]))} - -function OpenAPI.check_required(o::Dog) - o.pet_type === nothing && (return false) - true -end - -function OpenAPI.validate_properties(o::Dog) - OpenAPI.validate_property(Dog, Symbol("pet_type"), o.pet_type) - OpenAPI.validate_property(Dog, Symbol("bark"), o.bark) - OpenAPI.validate_property(Dog, Symbol("breed"), o.breed) -end - -function OpenAPI.validate_property(::Type{ Dog }, name::Symbol, val) - - - - if name === Symbol("breed") - OpenAPI.validate_param(name, "Dog", :enum, val, ["Dingo", "Husky", "Retriever", "Shepherd"]) - end - -end diff --git a/test/client/allany/AllAnyClient/src/models/model_DogAllOf.jl b/test/client/allany/AllAnyClient/src/models/model_DogAllOf.jl deleted file mode 100644 index 06eebd4..0000000 --- a/test/client/allany/AllAnyClient/src/models/model_DogAllOf.jl +++ /dev/null @@ -1,38 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Dog_allOf - - DogAllOf(; - bark=nothing, - breed=nothing, - ) - - - bark::Bool - - breed::String -""" -Base.@kwdef mutable struct DogAllOf <: OpenAPI.APIModel - bark::Union{Nothing, Bool} = nothing - breed::Union{Nothing, String} = nothing - - function DogAllOf(bark, breed, ) - OpenAPI.validate_property(DogAllOf, Symbol("bark"), bark) - OpenAPI.validate_property(DogAllOf, Symbol("breed"), breed) - return new(bark, breed, ) - end -end # type DogAllOf - -const _property_types_DogAllOf = Dict{Symbol,String}(Symbol("bark")=>"Bool", Symbol("breed")=>"String", ) -OpenAPI.property_type(::Type{ DogAllOf }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_DogAllOf[name]))} - -function check_required(o::DogAllOf) - true -end - -function OpenAPI.validate_property(::Type{ DogAllOf }, name::Symbol, val) - if name === Symbol("breed") - OpenAPI.validate_param(name, "DogAllOf", :enum, val, ["Dingo", "Husky", "Retriever", "Shepherd"]) - end -end - diff --git a/test/client/allany/AllAnyClient/src/models/model_OneOfBaseType.jl b/test/client/allany/AllAnyClient/src/models/model_OneOfBaseType.jl deleted file mode 100644 index 073188f..0000000 --- a/test/client/allany/AllAnyClient/src/models/model_OneOfBaseType.jl +++ /dev/null @@ -1,20 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - - -@doc raw"""OneOfBaseType - - OneOfBaseType(; value=nothing) -""" -mutable struct OneOfBaseType <: OpenAPI.OneOfAPIModel - value::Any # Union{ Float64, String } - OneOfBaseType() = new() - OneOfBaseType(value) = new(value) -end # type OneOfBaseType - -function OpenAPI.property_type(::Type{ OneOfBaseType }, name::Symbol, json::Dict{String,Any}) - - # no discriminator specified, can't determine the exact type - return fieldtype(OneOfBaseType, name) -end diff --git a/test/client/allany/AllAnyClient/src/models/model_OneOfMappedPets.jl b/test/client/allany/AllAnyClient/src/models/model_OneOfMappedPets.jl deleted file mode 100644 index afe696f..0000000 --- a/test/client/allany/AllAnyClient/src/models/model_OneOfMappedPets.jl +++ /dev/null @@ -1,24 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - - -@doc raw"""OneOfMappedPets - - OneOfMappedPets(; value=nothing) -""" -mutable struct OneOfMappedPets <: OpenAPI.OneOfAPIModel - value::Any # Union{ Cat, Dog } - OneOfMappedPets() = new() - OneOfMappedPets(value) = new(value) -end # type OneOfMappedPets - -function OpenAPI.property_type(::Type{ OneOfMappedPets }, name::Symbol, json::Dict{String,Any}) - discriminator = json["pet_type"] - if discriminator == "cat" - return eval(Base.Meta.parse("Cat")) - elseif discriminator == "dog" - return eval(Base.Meta.parse("Dog")) - end - throw(OpenAPI.ValidationException("Invalid discriminator value: $discriminator for OneOfMappedPets")) -end diff --git a/test/client/allany/AllAnyClient/src/models/model_OneOfPets.jl b/test/client/allany/AllAnyClient/src/models/model_OneOfPets.jl deleted file mode 100644 index a3fe448..0000000 --- a/test/client/allany/AllAnyClient/src/models/model_OneOfPets.jl +++ /dev/null @@ -1,24 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - - -@doc raw"""OneOfPets - - OneOfPets(; value=nothing) -""" -mutable struct OneOfPets <: OpenAPI.OneOfAPIModel - value::Any # Union{ Cat, Dog } - OneOfPets() = new() - OneOfPets(value) = new(value) -end # type OneOfPets - -function OpenAPI.property_type(::Type{ OneOfPets }, name::Symbol, json::Dict{String,Any}) - discriminator = json["pet_type"] - if discriminator == "Cat" - return eval(Base.Meta.parse("Cat")) - elseif discriminator == "Dog" - return eval(Base.Meta.parse("Dog")) - end - throw(OpenAPI.ValidationException("Invalid discriminator value: $discriminator for OneOfPets")) -end diff --git a/test/client/allany/AllAnyClient/src/models/model_Pet.jl b/test/client/allany/AllAnyClient/src/models/model_Pet.jl deleted file mode 100644 index e95c0d4..0000000 --- a/test/client/allany/AllAnyClient/src/models/model_Pet.jl +++ /dev/null @@ -1,37 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Pet - - Pet(; - pet_type=nothing, - ) - - - pet_type::String -""" -Base.@kwdef mutable struct Pet <: OpenAPI.APIModel - pet_type::Union{Nothing, String} = nothing - - function Pet(pet_type, ) - o = new(pet_type, ) - OpenAPI.validate_properties(o) - return o - end -end # type Pet - -const _property_types_Pet = Dict{Symbol,String}(Symbol("pet_type")=>"String", ) -OpenAPI.property_type(::Type{ Pet }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Pet[name]))} - -function OpenAPI.check_required(o::Pet) - o.pet_type === nothing && (return false) - true -end - -function OpenAPI.validate_properties(o::Pet) - OpenAPI.validate_property(Pet, Symbol("pet_type"), o.pet_type) -end - -function OpenAPI.validate_property(::Type{ Pet }, name::Symbol, val) - -end diff --git a/test/client/allany/AllAnyClient/src/models/model_TypeWithAllArrayTypes.jl b/test/client/allany/AllAnyClient/src/models/model_TypeWithAllArrayTypes.jl deleted file mode 100644 index 5236187..0000000 --- a/test/client/allany/AllAnyClient/src/models/model_TypeWithAllArrayTypes.jl +++ /dev/null @@ -1,51 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""TypeWithAllArrayTypes - - TypeWithAllArrayTypes(; - oneofbase=nothing, - anyofbase=nothing, - oneofpets=nothing, - anyofpets=nothing, - ) - - - oneofbase::Vector{OneOfBaseType} - - anyofbase::Vector{AnyOfBaseType} - - oneofpets::Vector{OneOfPets} - - anyofpets::Vector{AnyOfPets} -""" -Base.@kwdef mutable struct TypeWithAllArrayTypes <: OpenAPI.APIModel - oneofbase::Union{Nothing, Vector} = nothing # spec type: Union{ Nothing, Vector{OneOfBaseType} } - anyofbase::Union{Nothing, Vector} = nothing # spec type: Union{ Nothing, Vector{AnyOfBaseType} } - oneofpets::Union{Nothing, Vector} = nothing # spec type: Union{ Nothing, Vector{OneOfPets} } - anyofpets::Union{Nothing, Vector} = nothing # spec type: Union{ Nothing, Vector{AnyOfPets} } - - function TypeWithAllArrayTypes(oneofbase, anyofbase, oneofpets, anyofpets, ) - o = new(oneofbase, anyofbase, oneofpets, anyofpets, ) - OpenAPI.validate_properties(o) - return o - end -end # type TypeWithAllArrayTypes - -const _property_types_TypeWithAllArrayTypes = Dict{Symbol,String}(Symbol("oneofbase")=>"Vector{OneOfBaseType}", Symbol("anyofbase")=>"Vector{AnyOfBaseType}", Symbol("oneofpets")=>"Vector{OneOfPets}", Symbol("anyofpets")=>"Vector{AnyOfPets}", ) -OpenAPI.property_type(::Type{ TypeWithAllArrayTypes }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_TypeWithAllArrayTypes[name]))} - -function OpenAPI.check_required(o::TypeWithAllArrayTypes) - true -end - -function OpenAPI.validate_properties(o::TypeWithAllArrayTypes) - OpenAPI.validate_property(TypeWithAllArrayTypes, Symbol("oneofbase"), o.oneofbase) - OpenAPI.validate_property(TypeWithAllArrayTypes, Symbol("anyofbase"), o.anyofbase) - OpenAPI.validate_property(TypeWithAllArrayTypes, Symbol("oneofpets"), o.oneofpets) - OpenAPI.validate_property(TypeWithAllArrayTypes, Symbol("anyofpets"), o.anyofpets) -end - -function OpenAPI.validate_property(::Type{ TypeWithAllArrayTypes }, name::Symbol, val) - - - - -end diff --git a/test/client/allany/generate.sh b/test/client/allany/generate.sh deleted file mode 100755 index 9c8843b..0000000 --- a/test/client/allany/generate.sh +++ /dev/null @@ -1,5 +0,0 @@ -java -jar openapi-generator-cli.jar generate \ - -i ../../specs/allany.yaml \ - -g julia-client \ - -o AllAnyClient \ - --additional-properties=packageName=AllAnyClient diff --git a/test/client/allany/runtests.jl b/test/client/allany/runtests.jl deleted file mode 100644 index bd8cd88..0000000 --- a/test/client/allany/runtests.jl +++ /dev/null @@ -1,186 +0,0 @@ -module AllAnyTests - -include(joinpath(@__DIR__, "AllAnyClient", "src", "AllAnyClient.jl")) -using .AllAnyClient -using Test -using JSON -using HTTP -using OpenAPI -using OpenAPI.Clients -import OpenAPI.Clients: Client - -const M = AllAnyClient -const server = "http://127.0.0.1:8081" - -const mapped_cat = M.Cat(pet_type="cat", hunts=true, age=5) -const mapped_dog = M.Dog(pet_type="dog", bark=true, breed="Husky") -const cat = M.Cat(pet_type="Cat", hunts=true, age=5) -const dog = M.Dog(pet_type="Dog", bark=true, breed="Husky") - -function pet_equals(pet1, pet2) - @warn("pet_equals not implemented for $(typeof(pet1)) and $(typeof(pet2))") - false -end -function pet_equals(cat1::M.Cat, cat2::M.Cat) - cat1.pet_type == cat2.pet_type && cat1.hunts == cat2.hunts && cat1.age == cat2.age -end -function pet_equals(dog1::M.Dog, dog2::M.Dog) - dog1.pet_type == dog2.pet_type && dog1.bark == dog2.bark && dog1.breed == dog2.breed -end -pet_equals(pet1::OpenAPI.UnionAPIModel, pet2::OpenAPI.UnionAPIModel) = pet_equals(pet1.value, pet2.value) -basetype_equals(val1::OpenAPI.UnionAPIModel, val2::OpenAPI.UnionAPIModel) = val1.value == val2.value - -function runtests(httplib::Symbol) - @testset "allany" begin - @info("AllAnyApi ($httplib backend)") - client = Client(server; httplib=httplib) - api = M.DefaultApi(client) - - pet = M.AnyOfMappedPets(mapped_cat) - api_return, http_resp = echo_anyof_mapped_pets_post(api, pet) - @test pet_equals(api_return, pet) - - pet = M.AnyOfPets(dog) - api_return, http_resp = echo_anyof_pets_post(api, pet) - @test pet_equals(api_return, pet) - - pet = M.OneOfMappedPets(mapped_dog) - api_return, http_resp = echo_oneof_mapped_pets_post(api, pet) - @test pet_equals(api_return, pet) - - pet = M.OneOfPets(cat) - api_return, http_resp = echo_oneof_pets_post(api, pet) - @test pet_equals(api_return, pet) - - val = M.AnyOfBaseType("hello") - api_return, http_resp = echo_anyof_base_type_post(api, val) - @test basetype_equals(api_return, val) - - val = M.OneOfBaseType(100.1) - api_return, http_resp = echo_oneof_base_type_post(api, val) - @test basetype_equals(api_return, val) - - arr = M.TypeWithAllArrayTypes() - arr.oneofbase = [M.OneOfBaseType(1), M.OneOfBaseType(2)] - arr.anyofbase = [M.AnyOfBaseType("hello"), M.AnyOfBaseType("world")] - arr.oneofpets = [M.OneOfPets(cat), M.OneOfPets(dog)] - arr.anyofpets = [M.AnyOfPets(cat), M.AnyOfPets(dog)] - - api_return, http_resp = echo_arrays_post(api, arr) - - for idx in 1:2 - @test basetype_equals(arr.oneofbase[idx], api_return.oneofbase[idx]) - @test basetype_equals(arr.anyofbase[idx], api_return.anyofbase[idx]) - @test pet_equals(arr.oneofpets[idx], api_return.oneofpets[idx]) - @test pet_equals(arr.anyofpets[idx], api_return.anyofpets[idx]) - end - end -end - -function test_debug(httplib::Symbol) - @testset "stderr verbose mode" begin - @info("stderr verbose mode ($httplib backend)") - client = Client(server; - verbose=true, - httplib=httplib, - ) - api = M.DefaultApi(client) - - # HTTP.jl 2.x routes `verbose=true` output to stdout in a "[http] ... via h1" - # format, whereas 1.x (and the Downloads/curl backend) write the raw exchange to - # stderr. Capture the right stream and assert on the matching format per version. - use_stdout = httplib === :http && OpenAPI.Clients._HTTP_V2 - pipe = Pipe() - redirect = use_stdout ? redirect_stdout : redirect_stderr - redirect(pipe) do - pet = M.AnyOfMappedPets(mapped_cat) - api_return, http_resp = echo_anyof_mapped_pets_post(api, pet) - @test pet_equals(api_return, pet) - end - # Close the write end so the read sees EOF; without this, `readavailable` blocks - # forever when nothing was written to the captured stream. - close(pipe.in) - out_str = read(pipe, String) - if use_stdout - @test occursin("[http]", out_str) && occursin("200", out_str) - else - @test occursin("HTTP/1.1 200 OK", out_str) - end - end - - if httplib === :downloads - @testset "debug log verbose mode" begin - @info("debug log verbose mode") - client = Client(server; - verbose=OpenAPI.Clients.default_debug_hook, - httplib=httplib, - ) - api = M.DefaultApi(client) - - pipe = Pipe() - redirect_stderr(pipe) do - pet = M.AnyOfMappedPets(mapped_cat) - api_return, http_resp = echo_anyof_mapped_pets_post(api, pet) - @test pet_equals(api_return, pet) - end - out_str = String(readavailable(pipe)) - @test occursin("HTTP/1.1 200 OK", out_str) - end - @testset "custom verbose function" begin - @info("custom verbose function") - messages = Any[] - client = Client(server; - verbose=(type,message)->push!(messages, (type,message)), - httplib=httplib, - ) - api = M.DefaultApi(client) - - pet = M.AnyOfMappedPets(mapped_cat) - api_return, http_resp = echo_anyof_mapped_pets_post(api, pet) - @test pet_equals(api_return, pet) - - data_out = filter(messages) do elem - elem[1] == "DATA OUT" - end - @test !isempty(data_out) - iob = IOBuffer() - for (type, message) in data_out - write(iob, message) - end - data_out_str = String(take!(iob)) - data_out_json = JSON.parse(data_out_str) - @test data_out_json["pet_type"] == "cat" - @test data_out_json["hunts"] == true - @test data_out_json["age"] == 5 - - data_in = filter(messages) do elem - elem[1] == "DATA IN" - end - @test !isempty(data_in) - iob = IOBuffer() - for (type, message) in data_in - write(iob, message) - end - data_in_str = String(take!(iob)) - # The curl backend reports the raw response body. HTTP.jl 1.x servers send it - # chunk-framed ("27\r\n{json}\r\n0\r\n\r\n"); 2.x sends an unframed body with a - # Content-Length. Extract the JSON object itself so the parse works either way. - data_in_str = data_in_str[findfirst('{', data_in_str):findlast('}', data_in_str)] - data_in_json = JSON.parse(data_in_str) - @test data_in_json["pet_type"] == "cat" - @test data_in_json["hunts"] == true - @test data_in_json["age"] == 5 - end - end -end - -function test_http_resp() - resp = HTTP.Response(200, dog) - - @test resp.status == 200 - @test resp.headers == ["Content-Type" => "application/json"] - json = JSON.parse(String(copy(resp.body))) - @test pet_equals(OpenAPI.Clients.from_json(M.Dog, json), dog) -end - -end # module AllAnyTests diff --git a/test/client/openapigenerator_petstore_v3/generate.sh b/test/client/openapigenerator_petstore_v3/generate.sh deleted file mode 100755 index b2dae6e..0000000 --- a/test/client/openapigenerator_petstore_v3/generate.sh +++ /dev/null @@ -1,7 +0,0 @@ -java -jar openapi-generator-cli.jar generate \ - -i ../../specs/openapigenerator_petstore_v3.json \ - -g julia-client \ - -o petstore \ - --additional-properties=packageName=OpenAPIGenPetStoreClient \ - --additional-properties=exportModels=true \ - --additional-properties=exportOperations=true diff --git a/test/client/openapigenerator_petstore_v3/petstore/.openapi-generator-ignore b/test/client/openapigenerator_petstore_v3/petstore/.openapi-generator-ignore deleted file mode 100644 index 7484ee5..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/test/client/openapigenerator_petstore_v3/petstore/.openapi-generator/FILES b/test/client/openapigenerator_petstore_v3/petstore/.openapi-generator/FILES deleted file mode 100644 index 0a37d59..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/.openapi-generator/FILES +++ /dev/null @@ -1,21 +0,0 @@ -README.md -docs/ApiResponse.md -docs/Category.md -docs/Order.md -docs/Pet.md -docs/PetApi.md -docs/StoreApi.md -docs/Tag.md -docs/User.md -docs/UserApi.md -src/OpenAPIGenPetStoreClient.jl -src/apis/api_PetApi.jl -src/apis/api_StoreApi.jl -src/apis/api_UserApi.jl -src/modelincludes.jl -src/models/model_ApiResponse.jl -src/models/model_Category.jl -src/models/model_Order.jl -src/models/model_Pet.jl -src/models/model_Tag.jl -src/models/model_User.jl diff --git a/test/client/openapigenerator_petstore_v3/petstore/.openapi-generator/VERSION b/test/client/openapigenerator_petstore_v3/petstore/.openapi-generator/VERSION deleted file mode 100644 index 96cfbb1..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.13.0-SNAPSHOT diff --git a/test/client/openapigenerator_petstore_v3/petstore/README.md b/test/client/openapigenerator_petstore_v3/petstore/README.md deleted file mode 100644 index 498b243..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# Julia API client for OpenAPIGenPetStoreClient - -This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. - -## Overview -This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. - -- API version: 1.0.0 -- Generator version: 7.13.0-SNAPSHOT -- Build package: org.openapitools.codegen.languages.JuliaClientCodegen - - -## Installation -Place the Julia files generated under the `src` folder in your Julia project. Include OpenAPIGenPetStoreClient.jl in the project code. -It would include the module named OpenAPIGenPetStoreClient. - -Documentation is generated as markdown files under the `docs` folder. You can include them in your project documentation. -Documentation is also embedded in Julia which can be used with a Julia specific documentation generator. - -## API Endpoints - -Class | Method ------------- | ------------- -*PetApi* | [**add_pet**](docs/PetApi.md#add_pet)
**POST** /pet
Add a new pet to the store -*PetApi* | [**delete_pet**](docs/PetApi.md#delete_pet)
**DELETE** /pet/{petId}
Deletes a pet -*PetApi* | [**find_pets_by_status**](docs/PetApi.md#find_pets_by_status)
**GET** /pet/findByStatus
Finds Pets by status -*PetApi* | [**find_pets_by_tags**](docs/PetApi.md#find_pets_by_tags)
**GET** /pet/findByTags
Finds Pets by tags -*PetApi* | [**get_pet_by_id**](docs/PetApi.md#get_pet_by_id)
**GET** /pet/{petId}
Find pet by ID -*PetApi* | [**update_pet**](docs/PetApi.md#update_pet)
**PUT** /pet
Update an existing pet -*PetApi* | [**update_pet_with_form**](docs/PetApi.md#update_pet_with_form)
**POST** /pet/{petId}
Updates a pet in the store with form data -*PetApi* | [**upload_file**](docs/PetApi.md#upload_file)
**POST** /pet/{petId}/uploadImage
uploads an image -*StoreApi* | [**delete_order**](docs/StoreApi.md#delete_order)
**DELETE** /store/order/{orderId}
Delete purchase order by ID -*StoreApi* | [**get_inventory**](docs/StoreApi.md#get_inventory)
**GET** /store/inventory
Returns pet inventories by status -*StoreApi* | [**get_order_by_id**](docs/StoreApi.md#get_order_by_id)
**GET** /store/order/{orderId}
Find purchase order by ID -*StoreApi* | [**place_order**](docs/StoreApi.md#place_order)
**POST** /store/order
Place an order for a pet -*UserApi* | [**create_user**](docs/UserApi.md#create_user)
**POST** /user
Create user -*UserApi* | [**create_users_with_array_input**](docs/UserApi.md#create_users_with_array_input)
**POST** /user/createWithArray
Creates list of users with given input array -*UserApi* | [**create_users_with_list_input**](docs/UserApi.md#create_users_with_list_input)
**POST** /user/createWithList
Creates list of users with given input array -*UserApi* | [**delete_user**](docs/UserApi.md#delete_user)
**DELETE** /user/{username}
Delete user -*UserApi* | [**get_user_by_name**](docs/UserApi.md#get_user_by_name)
**GET** /user/{username}
Get user by user name -*UserApi* | [**login_user**](docs/UserApi.md#login_user)
**GET** /user/login
Logs user into the system -*UserApi* | [**logout_user**](docs/UserApi.md#logout_user)
**GET** /user/logout
Logs out current logged in user session -*UserApi* | [**update_user**](docs/UserApi.md#update_user)
**PUT** /user/{username}
Updated user - - -## Models - - - [ApiResponse](docs/ApiResponse.md) - - [Category](docs/Category.md) - - [Order](docs/Order.md) - - [Pet](docs/Pet.md) - - [Tag](docs/Tag.md) - - [User](docs/User.md) - - - -## Authorization - -Authentication schemes defined for the API: - -### petstore_auth -- **Type**: OAuth -- **Flow**: implicit -- **Authorization URL**: http://petstore.swagger.io/api/oauth/dialog -- **Scopes**: - - **read:pets**: read your pets - - **write:pets**: modify pets in your account - -Example -``` - using OpenAPI - using OpenAPI.Clients - import OpenAPI.Clients: Client, set_header - client = Client(server_uri) - set_header(client, "Authorization", "Bearer $bearer_auth") - api = MyApi(client) - result = callApi(api, args...; api_key) -``` - -### api_key -- **Type**: API key - -Example -``` - using OpenAPI - using OpenAPI.Clients - import OpenAPI.Clients: Client - client = Client(server_uri) - api = MyApi(client) - result = callApi(api, args...; api_key) -``` - -## Author - - - diff --git a/test/client/openapigenerator_petstore_v3/petstore/docs/ApiResponse.md b/test/client/openapigenerator_petstore_v3/petstore/docs/ApiResponse.md deleted file mode 100644 index a7a2c11..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/docs/ApiResponse.md +++ /dev/null @@ -1,14 +0,0 @@ -# ApiResponse - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**message** | **String** | | [optional] [default to nothing] -**code** | **Int64** | | [optional] [default to nothing] -**type** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/openapigenerator_petstore_v3/petstore/docs/Category.md b/test/client/openapigenerator_petstore_v3/petstore/docs/Category.md deleted file mode 100644 index e454c3b..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/docs/Category.md +++ /dev/null @@ -1,13 +0,0 @@ -# Category - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**name** | **String** | | [optional] [default to nothing] -**id** | **Int64** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/openapigenerator_petstore_v3/petstore/docs/Order.md b/test/client/openapigenerator_petstore_v3/petstore/docs/Order.md deleted file mode 100644 index 98c1bae..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/docs/Order.md +++ /dev/null @@ -1,17 +0,0 @@ -# Order - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**petId** | **Int64** | | [optional] [default to nothing] -**shipDate** | **ZonedDateTime** | | [optional] [default to nothing] -**status** | **String** | Order Status | [optional] [default to nothing] -**id** | **Int64** | | [optional] [default to nothing] -**complete** | **Bool** | | [optional] [default to false] -**quantity** | **Int64** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/openapigenerator_petstore_v3/petstore/docs/Pet.md b/test/client/openapigenerator_petstore_v3/petstore/docs/Pet.md deleted file mode 100644 index dbdd2db..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/docs/Pet.md +++ /dev/null @@ -1,17 +0,0 @@ -# Pet - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**name** | **String** | | [default to nothing] -**status** | **String** | pet status in the store | [optional] [default to nothing] -**id** | **Int64** | | [optional] [default to nothing] -**photoUrls** | **Vector{String}** | | [default to nothing] -**tags** | [**Vector{Tag}**](Tag.md) | | [optional] [default to nothing] -**category** | [***Category**](Category.md) | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/openapigenerator_petstore_v3/petstore/docs/PetApi.md b/test/client/openapigenerator_petstore_v3/petstore/docs/PetApi.md deleted file mode 100644 index 56b2a75..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/docs/PetApi.md +++ /dev/null @@ -1,276 +0,0 @@ -# PetApi - -All URIs are relative to */v3* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**add_pet**](PetApi.md#add_pet) | **POST** /pet | Add a new pet to the store -[**delete_pet**](PetApi.md#delete_pet) | **DELETE** /pet/{petId} | Deletes a pet -[**find_pets_by_status**](PetApi.md#find_pets_by_status) | **GET** /pet/findByStatus | Finds Pets by status -[**find_pets_by_tags**](PetApi.md#find_pets_by_tags) | **GET** /pet/findByTags | Finds Pets by tags -[**get_pet_by_id**](PetApi.md#get_pet_by_id) | **GET** /pet/{petId} | Find pet by ID -[**update_pet**](PetApi.md#update_pet) | **PUT** /pet | Update an existing pet -[**update_pet_with_form**](PetApi.md#update_pet_with_form) | **POST** /pet/{petId} | Updates a pet in the store with form data -[**upload_file**](PetApi.md#upload_file) | **POST** /pet/{petId}/uploadImage | uploads an image - - -# **add_pet** -> add_pet(_api::PetApi, pet::Pet; _mediaType=nothing) -> Pet, OpenAPI.Clients.ApiResponse
-> add_pet(_api::PetApi, response_stream::Channel, pet::Pet; _mediaType=nothing) -> Channel{ Pet }, OpenAPI.Clients.ApiResponse - -Add a new pet to the store - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**pet** | [**Pet**](Pet.md) | Pet object that needs to be added to the store | - -### Return type - -[**Pet**](Pet.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: application/xml, application/json - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **delete_pet** -> delete_pet(_api::PetApi, pet_id::Int64; api_key=nothing, _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> delete_pet(_api::PetApi, response_stream::Channel, pet_id::Int64; api_key=nothing, _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Deletes a pet - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**pet_id** | **Int64** | Pet id to delete | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **api_key** | **String** | | [default to nothing] - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **find_pets_by_status** -> find_pets_by_status(_api::PetApi, status::Vector{String}; _mediaType=nothing) -> Vector{Pet}, OpenAPI.Clients.ApiResponse
-> find_pets_by_status(_api::PetApi, response_stream::Channel, status::Vector{String}; _mediaType=nothing) -> Channel{ Vector{Pet} }, OpenAPI.Clients.ApiResponse - -Finds Pets by status - -Multiple status values can be provided with comma separated strings - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**status** | [**Vector{String}**](String.md) | Status values that need to be considered for filter | - -### Return type - -[**Vector{Pet}**](Pet.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **find_pets_by_tags** -> find_pets_by_tags(_api::PetApi, tags::Vector{String}; _mediaType=nothing) -> Vector{Pet}, OpenAPI.Clients.ApiResponse
-> find_pets_by_tags(_api::PetApi, response_stream::Channel, tags::Vector{String}; _mediaType=nothing) -> Channel{ Vector{Pet} }, OpenAPI.Clients.ApiResponse - -Finds Pets by tags - -Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**tags** | [**Vector{String}**](String.md) | Tags to filter by | - -### Return type - -[**Vector{Pet}**](Pet.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **get_pet_by_id** -> get_pet_by_id(_api::PetApi, pet_id::Int64; _mediaType=nothing) -> Pet, OpenAPI.Clients.ApiResponse
-> get_pet_by_id(_api::PetApi, response_stream::Channel, pet_id::Int64; _mediaType=nothing) -> Channel{ Pet }, OpenAPI.Clients.ApiResponse - -Find pet by ID - -Returns a single pet - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**pet_id** | **Int64** | ID of pet to return | - -### Return type - -[**Pet**](Pet.md) - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **update_pet** -> update_pet(_api::PetApi, pet::Pet; _mediaType=nothing) -> Pet, OpenAPI.Clients.ApiResponse
-> update_pet(_api::PetApi, response_stream::Channel, pet::Pet; _mediaType=nothing) -> Channel{ Pet }, OpenAPI.Clients.ApiResponse - -Update an existing pet - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**pet** | [**Pet**](Pet.md) | Pet object that needs to be added to the store | - -### Return type - -[**Pet**](Pet.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: application/xml, application/json - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **update_pet_with_form** -> update_pet_with_form(_api::PetApi, pet_id::Int64; name=nothing, status=nothing, _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> update_pet_with_form(_api::PetApi, response_stream::Channel, pet_id::Int64; name=nothing, status=nothing, _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Updates a pet in the store with form data - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**pet_id** | **Int64** | ID of pet that needs to be updated | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **name** | **String** | Updated name of the pet | [default to nothing] - **status** | **String** | Updated status of the pet | [default to nothing] - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: application/x-www-form-urlencoded - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **upload_file** -> upload_file(_api::PetApi, pet_id::Int64; file=nothing, additional_metadata=nothing, _mediaType=nothing) -> ApiResponse, OpenAPI.Clients.ApiResponse
-> upload_file(_api::PetApi, response_stream::Channel, pet_id::Int64; file=nothing, additional_metadata=nothing, _mediaType=nothing) -> Channel{ ApiResponse }, OpenAPI.Clients.ApiResponse - -uploads an image - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**pet_id** | **Int64** | ID of pet to update | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **file** | **String** | file to upload | - **additional_metadata** | **String** | Additional data to pass to server | [default to nothing] - -### Return type - -[**ApiResponse**](ApiResponse.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: multipart/form-data - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - diff --git a/test/client/openapigenerator_petstore_v3/petstore/docs/StoreApi.md b/test/client/openapigenerator_petstore_v3/petstore/docs/StoreApi.md deleted file mode 100644 index 659d144..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/docs/StoreApi.md +++ /dev/null @@ -1,128 +0,0 @@ -# StoreApi - -All URIs are relative to */v3* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**delete_order**](StoreApi.md#delete_order) | **DELETE** /store/order/{orderId} | Delete purchase order by ID -[**get_inventory**](StoreApi.md#get_inventory) | **GET** /store/inventory | Returns pet inventories by status -[**get_order_by_id**](StoreApi.md#get_order_by_id) | **GET** /store/order/{orderId} | Find purchase order by ID -[**place_order**](StoreApi.md#place_order) | **POST** /store/order | Place an order for a pet - - -# **delete_order** -> delete_order(_api::StoreApi, order_id::String; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> delete_order(_api::StoreApi, response_stream::Channel, order_id::String; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Delete purchase order by ID - -For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **StoreApi** | API context | -**order_id** | **String** | ID of the order that needs to be deleted | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **get_inventory** -> get_inventory(_api::StoreApi; _mediaType=nothing) -> Dict{String, Int64}, OpenAPI.Clients.ApiResponse
-> get_inventory(_api::StoreApi, response_stream::Channel; _mediaType=nothing) -> Channel{ Dict{String, Int64} }, OpenAPI.Clients.ApiResponse - -Returns pet inventories by status - -Returns a map of status codes to quantities - -### Required Parameters -This endpoint does not need any parameter. - -### Return type - -**Dict{String, Int64}** - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **get_order_by_id** -> get_order_by_id(_api::StoreApi, order_id::Int64; _mediaType=nothing) -> Order, OpenAPI.Clients.ApiResponse
-> get_order_by_id(_api::StoreApi, response_stream::Channel, order_id::Int64; _mediaType=nothing) -> Channel{ Order }, OpenAPI.Clients.ApiResponse - -Find purchase order by ID - -For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **StoreApi** | API context | -**order_id** | **Int64** | ID of pet that needs to be fetched | - -### Return type - -[**Order**](Order.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **place_order** -> place_order(_api::StoreApi, order::Order; _mediaType=nothing) -> Order, OpenAPI.Clients.ApiResponse
-> place_order(_api::StoreApi, response_stream::Channel, order::Order; _mediaType=nothing) -> Channel{ Order }, OpenAPI.Clients.ApiResponse - -Place an order for a pet - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **StoreApi** | API context | -**order** | [**Order**](Order.md) | order placed for purchasing the pet | - -### Return type - -[**Order**](Order.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - diff --git a/test/client/openapigenerator_petstore_v3/petstore/docs/Tag.md b/test/client/openapigenerator_petstore_v3/petstore/docs/Tag.md deleted file mode 100644 index ee2633c..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/docs/Tag.md +++ /dev/null @@ -1,13 +0,0 @@ -# Tag - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**name** | **String** | | [optional] [default to nothing] -**id** | **Int64** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/openapigenerator_petstore_v3/petstore/docs/User.md b/test/client/openapigenerator_petstore_v3/petstore/docs/User.md deleted file mode 100644 index 3db4060..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/docs/User.md +++ /dev/null @@ -1,19 +0,0 @@ -# User - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**password** | **String** | | [optional] [default to nothing] -**id** | **Int64** | | [optional] [default to nothing] -**username** | **String** | | [optional] [default to nothing] -**firstName** | **String** | | [optional] [default to nothing] -**lastName** | **String** | | [optional] [default to nothing] -**phone** | **String** | | [optional] [default to nothing] -**userStatus** | **Int64** | User Status | [optional] [default to nothing] -**email** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/openapigenerator_petstore_v3/petstore/docs/UserApi.md b/test/client/openapigenerator_petstore_v3/petstore/docs/UserApi.md deleted file mode 100644 index 5a7a243..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/docs/UserApi.md +++ /dev/null @@ -1,254 +0,0 @@ -# UserApi - -All URIs are relative to */v3* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**create_user**](UserApi.md#create_user) | **POST** /user | Create user -[**create_users_with_array_input**](UserApi.md#create_users_with_array_input) | **POST** /user/createWithArray | Creates list of users with given input array -[**create_users_with_list_input**](UserApi.md#create_users_with_list_input) | **POST** /user/createWithList | Creates list of users with given input array -[**delete_user**](UserApi.md#delete_user) | **DELETE** /user/{username} | Delete user -[**get_user_by_name**](UserApi.md#get_user_by_name) | **GET** /user/{username} | Get user by user name -[**login_user**](UserApi.md#login_user) | **GET** /user/login | Logs user into the system -[**logout_user**](UserApi.md#logout_user) | **GET** /user/logout | Logs out current logged in user session -[**update_user**](UserApi.md#update_user) | **PUT** /user/{username} | Updated user - - -# **create_user** -> create_user(_api::UserApi, user::User; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> create_user(_api::UserApi, response_stream::Channel, user::User; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Create user - -This can only be done by the logged in user. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**user** | [**User**](User.md) | Created user object | - -### Return type - -Nothing - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **create_users_with_array_input** -> create_users_with_array_input(_api::UserApi, user::Vector{User}; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> create_users_with_array_input(_api::UserApi, response_stream::Channel, user::Vector{User}; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Creates list of users with given input array - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**user** | [**Vector{User}**](User.md) | List of user object | - -### Return type - -Nothing - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **create_users_with_list_input** -> create_users_with_list_input(_api::UserApi, user::Vector{User}; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> create_users_with_list_input(_api::UserApi, response_stream::Channel, user::Vector{User}; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Creates list of users with given input array - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**user** | [**Vector{User}**](User.md) | List of user object | - -### Return type - -Nothing - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **delete_user** -> delete_user(_api::UserApi, username::String; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> delete_user(_api::UserApi, response_stream::Channel, username::String; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Delete user - -This can only be done by the logged in user. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**username** | **String** | The name that needs to be deleted | - -### Return type - -Nothing - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **get_user_by_name** -> get_user_by_name(_api::UserApi, username::String; _mediaType=nothing) -> User, OpenAPI.Clients.ApiResponse
-> get_user_by_name(_api::UserApi, response_stream::Channel, username::String; _mediaType=nothing) -> Channel{ User }, OpenAPI.Clients.ApiResponse - -Get user by user name - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**username** | **String** | The name that needs to be fetched. Use user1 for testing. | - -### Return type - -[**User**](User.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **login_user** -> login_user(_api::UserApi, username::String, password::String; _mediaType=nothing) -> String, OpenAPI.Clients.ApiResponse
-> login_user(_api::UserApi, response_stream::Channel, username::String, password::String; _mediaType=nothing) -> Channel{ String }, OpenAPI.Clients.ApiResponse - -Logs user into the system - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**username** | **String** | The user name for login | -**password** | **String** | The password for login in clear text | - -### Return type - -**String** - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **logout_user** -> logout_user(_api::UserApi; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> logout_user(_api::UserApi, response_stream::Channel; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Logs out current logged in user session - - - -### Required Parameters -This endpoint does not need any parameter. - -### Return type - -Nothing - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **update_user** -> update_user(_api::UserApi, username::String, user::User; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> update_user(_api::UserApi, response_stream::Channel, username::String, user::User; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Updated user - -This can only be done by the logged in user. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**username** | **String** | name that need to be deleted | -**user** | [**User**](User.md) | Updated user object | - -### Return type - -Nothing - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - diff --git a/test/client/openapigenerator_petstore_v3/petstore/src/OpenAPIGenPetStoreClient.jl b/test/client/openapigenerator_petstore_v3/petstore/src/OpenAPIGenPetStoreClient.jl deleted file mode 100644 index b4effe4..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/src/OpenAPIGenPetStoreClient.jl +++ /dev/null @@ -1,31 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -module OpenAPIGenPetStoreClient - -using Dates, TimeZones -using OpenAPI -using OpenAPI.Clients - -const API_VERSION = "1.0.0" - -include("modelincludes.jl") - -include("apis/api_PetApi.jl") -include("apis/api_StoreApi.jl") -include("apis/api_UserApi.jl") - -# export models -export ApiResponse -export Category -export Order -export Pet -export Tag -export User - -# export operations -export PetApi -export StoreApi -export UserApi - -end # module OpenAPIGenPetStoreClient diff --git a/test/client/openapigenerator_petstore_v3/petstore/src/apis/api_PetApi.jl b/test/client/openapigenerator_petstore_v3/petstore/src/apis/api_PetApi.jl deleted file mode 100644 index a4d9e1e..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/src/apis/api_PetApi.jl +++ /dev/null @@ -1,285 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -struct PetApi <: OpenAPI.APIClientImpl - client::OpenAPI.Clients.Client -end - -""" -The default API base path for APIs in `PetApi`. -This can be used to construct the `OpenAPI.Clients.Client` instance. -""" -basepath(::Type{ PetApi }) = "/v3" - -const _returntypes_add_pet_PetApi = Dict{Regex,Type}( - Regex("^" * replace("405", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("200", "x"=>".") * "\$") => Pet, -) - -function _oacinternal_add_pet(_api::PetApi, pet::Pet; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_add_pet_PetApi, "/pet", ["petstore_auth", ], pet) - OpenAPI.Clients.set_header_accept(_ctx, ["application/xml", "application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/xml", "application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Add a new pet to the store - - - -Params: -- pet::Pet (required) - -Return: Pet, OpenAPI.Clients.ApiResponse -""" -function add_pet(_api::PetApi, pet::Pet; _mediaType=nothing) - _ctx = _oacinternal_add_pet(_api, pet; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function add_pet(_api::PetApi, response_stream::Channel, pet::Pet; _mediaType=nothing) - _ctx = _oacinternal_add_pet(_api, pet; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_delete_pet_PetApi = Dict{Regex,Type}( - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_delete_pet(_api::PetApi, pet_id::Int64; api_key=nothing, _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "DELETE", _returntypes_delete_pet_PetApi, "/pet/{petId}", ["petstore_auth", ]) - OpenAPI.Clients.set_param(_ctx.path, "petId", pet_id) # type Int64 - OpenAPI.Clients.set_param(_ctx.header, "api_key", api_key) # type String - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Deletes a pet - - - -Params: -- pet_id::Int64 (required) -- api_key::String - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function delete_pet(_api::PetApi, pet_id::Int64; api_key=nothing, _mediaType=nothing) - _ctx = _oacinternal_delete_pet(_api, pet_id; api_key=api_key, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function delete_pet(_api::PetApi, response_stream::Channel, pet_id::Int64; api_key=nothing, _mediaType=nothing) - _ctx = _oacinternal_delete_pet(_api, pet_id; api_key=api_key, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_find_pets_by_status_PetApi = Dict{Regex,Type}( - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("200", "x"=>".") * "\$") => Vector{Pet}, -) - -function _oacinternal_find_pets_by_status(_api::PetApi, status::Vector{String}; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_find_pets_by_status_PetApi, "/pet/findByStatus", ["petstore_auth", ]) - OpenAPI.Clients.set_param(_ctx.query, "status", status; style="form", is_explode=false) # type Vector{String} - OpenAPI.Clients.set_header_accept(_ctx, ["application/xml", "application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Finds Pets by status - -Multiple status values can be provided with comma separated strings - -Params: -- status::Vector{String} (required) - -Return: Vector{Pet}, OpenAPI.Clients.ApiResponse -""" -function find_pets_by_status(_api::PetApi, status::Vector{String}; _mediaType=nothing) - _ctx = _oacinternal_find_pets_by_status(_api, status; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function find_pets_by_status(_api::PetApi, response_stream::Channel, status::Vector{String}; _mediaType=nothing) - _ctx = _oacinternal_find_pets_by_status(_api, status; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_find_pets_by_tags_PetApi = Dict{Regex,Type}( - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("200", "x"=>".") * "\$") => Vector{Pet}, -) - -function _oacinternal_find_pets_by_tags(_api::PetApi, tags::Vector{String}; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_find_pets_by_tags_PetApi, "/pet/findByTags", ["petstore_auth", ]) - OpenAPI.Clients.set_param(_ctx.query, "tags", tags; style="form", is_explode=false) # type Vector{String} - OpenAPI.Clients.set_header_accept(_ctx, ["application/xml", "application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Finds Pets by tags - -Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. - -Params: -- tags::Vector{String} (required) - -Return: Vector{Pet}, OpenAPI.Clients.ApiResponse -""" -function find_pets_by_tags(_api::PetApi, tags::Vector{String}; _mediaType=nothing) - _ctx = _oacinternal_find_pets_by_tags(_api, tags; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function find_pets_by_tags(_api::PetApi, response_stream::Channel, tags::Vector{String}; _mediaType=nothing) - _ctx = _oacinternal_find_pets_by_tags(_api, tags; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_get_pet_by_id_PetApi = Dict{Regex,Type}( - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("200", "x"=>".") * "\$") => Pet, -) - -function _oacinternal_get_pet_by_id(_api::PetApi, pet_id::Int64; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_get_pet_by_id_PetApi, "/pet/{petId}", ["api_key", ]) - OpenAPI.Clients.set_param(_ctx.path, "petId", pet_id) # type Int64 - OpenAPI.Clients.set_header_accept(_ctx, ["application/xml", "application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Find pet by ID - -Returns a single pet - -Params: -- pet_id::Int64 (required) - -Return: Pet, OpenAPI.Clients.ApiResponse -""" -function get_pet_by_id(_api::PetApi, pet_id::Int64; _mediaType=nothing) - _ctx = _oacinternal_get_pet_by_id(_api, pet_id; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function get_pet_by_id(_api::PetApi, response_stream::Channel, pet_id::Int64; _mediaType=nothing) - _ctx = _oacinternal_get_pet_by_id(_api, pet_id; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_update_pet_PetApi = Dict{Regex,Type}( - Regex("^" * replace("405", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("200", "x"=>".") * "\$") => Pet, -) - -function _oacinternal_update_pet(_api::PetApi, pet::Pet; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "PUT", _returntypes_update_pet_PetApi, "/pet", ["petstore_auth", ], pet) - OpenAPI.Clients.set_header_accept(_ctx, ["application/xml", "application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/xml", "application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Update an existing pet - - - -Params: -- pet::Pet (required) - -Return: Pet, OpenAPI.Clients.ApiResponse -""" -function update_pet(_api::PetApi, pet::Pet; _mediaType=nothing) - _ctx = _oacinternal_update_pet(_api, pet; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function update_pet(_api::PetApi, response_stream::Channel, pet::Pet; _mediaType=nothing) - _ctx = _oacinternal_update_pet(_api, pet; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_update_pet_with_form_PetApi = Dict{Regex,Type}( - Regex("^" * replace("405", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_update_pet_with_form(_api::PetApi, pet_id::Int64; name=nothing, status=nothing, _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_update_pet_with_form_PetApi, "/pet/{petId}", ["petstore_auth", ]) - OpenAPI.Clients.set_param(_ctx.path, "petId", pet_id) # type Int64 - OpenAPI.Clients.set_param(_ctx.form, "name", name) # type String - OpenAPI.Clients.set_param(_ctx.form, "status", status) # type String - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/x-www-form-urlencoded", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Updates a pet in the store with form data - - - -Params: -- pet_id::Int64 (required) -- name::String -- status::String - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function update_pet_with_form(_api::PetApi, pet_id::Int64; name=nothing, status=nothing, _mediaType=nothing) - _ctx = _oacinternal_update_pet_with_form(_api, pet_id; name=name, status=status, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function update_pet_with_form(_api::PetApi, response_stream::Channel, pet_id::Int64; name=nothing, status=nothing, _mediaType=nothing) - _ctx = _oacinternal_update_pet_with_form(_api, pet_id; name=name, status=status, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_upload_file_PetApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => ApiResponse, -) - -function _oacinternal_upload_file(_api::PetApi, pet_id::Int64; file=nothing, additional_metadata=nothing, _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_upload_file_PetApi, "/pet/{petId}/uploadImage", ["petstore_auth", ]) - OpenAPI.Clients.set_param(_ctx.path, "petId", pet_id) # type Int64 - OpenAPI.Clients.set_param(_ctx.file, "file", file) # type String - OpenAPI.Clients.set_param(_ctx.form, "additionalMetadata", additional_metadata) # type String - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["multipart/form-data", ] : [_mediaType]) - return _ctx -end - -@doc raw"""uploads an image - - - -Params: -- pet_id::Int64 (required) -- file::String -- additional_metadata::String - -Return: ApiResponse, OpenAPI.Clients.ApiResponse -""" -function upload_file(_api::PetApi, pet_id::Int64; file=nothing, additional_metadata=nothing, _mediaType=nothing) - _ctx = _oacinternal_upload_file(_api, pet_id; file=file, additional_metadata=additional_metadata, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function upload_file(_api::PetApi, response_stream::Channel, pet_id::Int64; file=nothing, additional_metadata=nothing, _mediaType=nothing) - _ctx = _oacinternal_upload_file(_api, pet_id; file=file, additional_metadata=additional_metadata, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -export add_pet -export delete_pet -export find_pets_by_status -export find_pets_by_tags -export get_pet_by_id -export update_pet -export update_pet_with_form -export upload_file diff --git a/test/client/openapigenerator_petstore_v3/petstore/src/apis/api_StoreApi.jl b/test/client/openapigenerator_petstore_v3/petstore/src/apis/api_StoreApi.jl deleted file mode 100644 index 0da61e1..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/src/apis/api_StoreApi.jl +++ /dev/null @@ -1,145 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -struct StoreApi <: OpenAPI.APIClientImpl - client::OpenAPI.Clients.Client -end - -""" -The default API base path for APIs in `StoreApi`. -This can be used to construct the `OpenAPI.Clients.Client` instance. -""" -basepath(::Type{ StoreApi }) = "/v3" - -const _returntypes_delete_order_StoreApi = Dict{Regex,Type}( - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_delete_order(_api::StoreApi, order_id::String; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "DELETE", _returntypes_delete_order_StoreApi, "/store/order/{orderId}", []) - OpenAPI.Clients.set_param(_ctx.path, "orderId", order_id) # type String - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Delete purchase order by ID - -For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors - -Params: -- order_id::String (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function delete_order(_api::StoreApi, order_id::String; _mediaType=nothing) - _ctx = _oacinternal_delete_order(_api, order_id; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function delete_order(_api::StoreApi, response_stream::Channel, order_id::String; _mediaType=nothing) - _ctx = _oacinternal_delete_order(_api, order_id; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_get_inventory_StoreApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => Dict{String, Int64}, -) - -function _oacinternal_get_inventory(_api::StoreApi; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_get_inventory_StoreApi, "/store/inventory", ["api_key", ]) - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Returns pet inventories by status - -Returns a map of status codes to quantities - -Params: - -Return: Dict{String, Int64}, OpenAPI.Clients.ApiResponse -""" -function get_inventory(_api::StoreApi; _mediaType=nothing) - _ctx = _oacinternal_get_inventory(_api; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function get_inventory(_api::StoreApi, response_stream::Channel; _mediaType=nothing) - _ctx = _oacinternal_get_inventory(_api; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_get_order_by_id_StoreApi = Dict{Regex,Type}( - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("200", "x"=>".") * "\$") => Order, -) - -function _oacinternal_get_order_by_id(_api::StoreApi, order_id::Int64; _mediaType=nothing) - OpenAPI.validate_param("order_id", "get_order_by_id", :maximum, order_id, 5, false) - OpenAPI.validate_param("order_id", "get_order_by_id", :minimum, order_id, 1, false) - - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_get_order_by_id_StoreApi, "/store/order/{orderId}", []) - OpenAPI.Clients.set_param(_ctx.path, "orderId", order_id) # type Int64 - OpenAPI.Clients.set_header_accept(_ctx, ["application/xml", "application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Find purchase order by ID - -For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions - -Params: -- order_id::Int64 (required) - -Return: Order, OpenAPI.Clients.ApiResponse -""" -function get_order_by_id(_api::StoreApi, order_id::Int64; _mediaType=nothing) - _ctx = _oacinternal_get_order_by_id(_api, order_id; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function get_order_by_id(_api::StoreApi, response_stream::Channel, order_id::Int64; _mediaType=nothing) - _ctx = _oacinternal_get_order_by_id(_api, order_id; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_place_order_StoreApi = Dict{Regex,Type}( - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("200", "x"=>".") * "\$") => Order, -) - -function _oacinternal_place_order(_api::StoreApi, order::Order; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_place_order_StoreApi, "/store/order", [], order) - OpenAPI.Clients.set_header_accept(_ctx, ["application/xml", "application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Place an order for a pet - - - -Params: -- order::Order (required) - -Return: Order, OpenAPI.Clients.ApiResponse -""" -function place_order(_api::StoreApi, order::Order; _mediaType=nothing) - _ctx = _oacinternal_place_order(_api, order; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function place_order(_api::StoreApi, response_stream::Channel, order::Order; _mediaType=nothing) - _ctx = _oacinternal_place_order(_api, order; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -export delete_order -export get_inventory -export get_order_by_id -export place_order diff --git a/test/client/openapigenerator_petstore_v3/petstore/src/apis/api_UserApi.jl b/test/client/openapigenerator_petstore_v3/petstore/src/apis/api_UserApi.jl deleted file mode 100644 index fb30046..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/src/apis/api_UserApi.jl +++ /dev/null @@ -1,274 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -struct UserApi <: OpenAPI.APIClientImpl - client::OpenAPI.Clients.Client -end - -""" -The default API base path for APIs in `UserApi`. -This can be used to construct the `OpenAPI.Clients.Client` instance. -""" -basepath(::Type{ UserApi }) = "/v3" - -const _returntypes_create_user_UserApi = Dict{Regex,Type}( - Regex("^" * replace("0", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_create_user(_api::UserApi, user::User; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_create_user_UserApi, "/user", ["api_key", ], user) - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Create user - -This can only be done by the logged in user. - -Params: -- user::User (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function create_user(_api::UserApi, user::User; _mediaType=nothing) - _ctx = _oacinternal_create_user(_api, user; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function create_user(_api::UserApi, response_stream::Channel, user::User; _mediaType=nothing) - _ctx = _oacinternal_create_user(_api, user; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_create_users_with_array_input_UserApi = Dict{Regex,Type}( - Regex("^" * replace("0", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_create_users_with_array_input(_api::UserApi, user::Vector{User}; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_create_users_with_array_input_UserApi, "/user/createWithArray", ["api_key", ], user) - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Creates list of users with given input array - - - -Params: -- user::Vector{User} (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function create_users_with_array_input(_api::UserApi, user::Vector{User}; _mediaType=nothing) - _ctx = _oacinternal_create_users_with_array_input(_api, user; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function create_users_with_array_input(_api::UserApi, response_stream::Channel, user::Vector{User}; _mediaType=nothing) - _ctx = _oacinternal_create_users_with_array_input(_api, user; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_create_users_with_list_input_UserApi = Dict{Regex,Type}( - Regex("^" * replace("0", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_create_users_with_list_input(_api::UserApi, user::Vector{User}; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_create_users_with_list_input_UserApi, "/user/createWithList", ["api_key", ], user) - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Creates list of users with given input array - - - -Params: -- user::Vector{User} (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function create_users_with_list_input(_api::UserApi, user::Vector{User}; _mediaType=nothing) - _ctx = _oacinternal_create_users_with_list_input(_api, user; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function create_users_with_list_input(_api::UserApi, response_stream::Channel, user::Vector{User}; _mediaType=nothing) - _ctx = _oacinternal_create_users_with_list_input(_api, user; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_delete_user_UserApi = Dict{Regex,Type}( - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_delete_user(_api::UserApi, username::String; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "DELETE", _returntypes_delete_user_UserApi, "/user/{username}", ["api_key", ]) - OpenAPI.Clients.set_param(_ctx.path, "username", username) # type String - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Delete user - -This can only be done by the logged in user. - -Params: -- username::String (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function delete_user(_api::UserApi, username::String; _mediaType=nothing) - _ctx = _oacinternal_delete_user(_api, username; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function delete_user(_api::UserApi, response_stream::Channel, username::String; _mediaType=nothing) - _ctx = _oacinternal_delete_user(_api, username; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_get_user_by_name_UserApi = Dict{Regex,Type}( - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("200", "x"=>".") * "\$") => User, -) - -function _oacinternal_get_user_by_name(_api::UserApi, username::String; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_get_user_by_name_UserApi, "/user/{username}", []) - OpenAPI.Clients.set_param(_ctx.path, "username", username) # type String - OpenAPI.Clients.set_header_accept(_ctx, ["application/xml", "application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Get user by user name - - - -Params: -- username::String (required) - -Return: User, OpenAPI.Clients.ApiResponse -""" -function get_user_by_name(_api::UserApi, username::String; _mediaType=nothing) - _ctx = _oacinternal_get_user_by_name(_api, username; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function get_user_by_name(_api::UserApi, response_stream::Channel, username::String; _mediaType=nothing) - _ctx = _oacinternal_get_user_by_name(_api, username; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_login_user_UserApi = Dict{Regex,Type}( - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("200", "x"=>".") * "\$") => String, -) - -function _oacinternal_login_user(_api::UserApi, username::String, password::String; _mediaType=nothing) - OpenAPI.validate_param("username", "login_user", :pattern, username, r"^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$") - - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_login_user_UserApi, "/user/login", []) - OpenAPI.Clients.set_param(_ctx.query, "username", username; style="form", is_explode=true) # type String - OpenAPI.Clients.set_param(_ctx.query, "password", password; style="form", is_explode=true) # type String - OpenAPI.Clients.set_header_accept(_ctx, ["application/xml", "application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Logs user into the system - - - -Params: -- username::String (required) -- password::String (required) - -Return: String, OpenAPI.Clients.ApiResponse -""" -function login_user(_api::UserApi, username::String, password::String; _mediaType=nothing) - _ctx = _oacinternal_login_user(_api, username, password; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function login_user(_api::UserApi, response_stream::Channel, username::String, password::String; _mediaType=nothing) - _ctx = _oacinternal_login_user(_api, username, password; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_logout_user_UserApi = Dict{Regex,Type}( - Regex("^" * replace("0", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_logout_user(_api::UserApi; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_logout_user_UserApi, "/user/logout", ["api_key", ]) - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Logs out current logged in user session - - - -Params: - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function logout_user(_api::UserApi; _mediaType=nothing) - _ctx = _oacinternal_logout_user(_api; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function logout_user(_api::UserApi, response_stream::Channel; _mediaType=nothing) - _ctx = _oacinternal_logout_user(_api; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_update_user_UserApi = Dict{Regex,Type}( - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_update_user(_api::UserApi, username::String, user::User; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "PUT", _returntypes_update_user_UserApi, "/user/{username}", ["api_key", ], user) - OpenAPI.Clients.set_param(_ctx.path, "username", username) # type String - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Updated user - -This can only be done by the logged in user. - -Params: -- username::String (required) -- user::User (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function update_user(_api::UserApi, username::String, user::User; _mediaType=nothing) - _ctx = _oacinternal_update_user(_api, username, user; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function update_user(_api::UserApi, response_stream::Channel, username::String, user::User; _mediaType=nothing) - _ctx = _oacinternal_update_user(_api, username, user; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -export create_user -export create_users_with_array_input -export create_users_with_list_input -export delete_user -export get_user_by_name -export login_user -export logout_user -export update_user diff --git a/test/client/openapigenerator_petstore_v3/petstore/src/modelincludes.jl b/test/client/openapigenerator_petstore_v3/petstore/src/modelincludes.jl deleted file mode 100644 index b3a3db8..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/src/modelincludes.jl +++ /dev/null @@ -1,9 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -include("models/model_ApiResponse.jl") -include("models/model_Category.jl") -include("models/model_Order.jl") -include("models/model_Pet.jl") -include("models/model_Tag.jl") -include("models/model_User.jl") diff --git a/test/client/openapigenerator_petstore_v3/petstore/src/models/model_ApiResponse.jl b/test/client/openapigenerator_petstore_v3/petstore/src/models/model_ApiResponse.jl deleted file mode 100644 index 80cfeac..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/src/models/model_ApiResponse.jl +++ /dev/null @@ -1,50 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""ApiResponse -Describes the result of uploading an image resource - - ApiResponse(; - message=nothing, - code=nothing, - type=nothing, - ) - - - message::String - - code::Int64 - - type::String -""" -Base.@kwdef mutable struct ApiResponse <: OpenAPI.APIModel - message::Union{Nothing, String} = nothing - code::Union{Nothing, Int64} = nothing - type::Union{Nothing, String} = nothing - - function ApiResponse(message, code, type, ) - o = new(message, code, type, ) - OpenAPI.validate_properties(o) - return o - end -end # type ApiResponse - -const _property_types_ApiResponse = Dict{Symbol,String}(Symbol("message")=>"String", Symbol("code")=>"Int64", Symbol("type")=>"String", ) -OpenAPI.property_type(::Type{ ApiResponse }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_ApiResponse[name]))} - -function OpenAPI.check_required(o::ApiResponse) - true -end - -function OpenAPI.validate_properties(o::ApiResponse) - OpenAPI.validate_property(ApiResponse, Symbol("message"), o.message) - OpenAPI.validate_property(ApiResponse, Symbol("code"), o.code) - OpenAPI.validate_property(ApiResponse, Symbol("type"), o.type) -end - -function OpenAPI.validate_property(::Type{ ApiResponse }, name::Symbol, val) - - - if name === Symbol("code") - OpenAPI.validate_param(name, "ApiResponse", :format, val, "int32") - end - -end diff --git a/test/client/openapigenerator_petstore_v3/petstore/src/models/model_Category.jl b/test/client/openapigenerator_petstore_v3/petstore/src/models/model_Category.jl deleted file mode 100644 index 53e027a..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/src/models/model_Category.jl +++ /dev/null @@ -1,48 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Category -A category for a pet - - Category(; - name=nothing, - id=nothing, - ) - - - name::String - - id::Int64 -""" -Base.@kwdef mutable struct Category <: OpenAPI.APIModel - name::Union{Nothing, String} = nothing - id::Union{Nothing, Int64} = nothing - - function Category(name, id, ) - o = new(name, id, ) - OpenAPI.validate_properties(o) - return o - end -end # type Category - -const _property_types_Category = Dict{Symbol,String}(Symbol("name")=>"String", Symbol("id")=>"Int64", ) -OpenAPI.property_type(::Type{ Category }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Category[name]))} - -function OpenAPI.check_required(o::Category) - true -end - -function OpenAPI.validate_properties(o::Category) - OpenAPI.validate_property(Category, Symbol("name"), o.name) - OpenAPI.validate_property(Category, Symbol("id"), o.id) -end - -function OpenAPI.validate_property(::Type{ Category }, name::Symbol, val) - - if name === Symbol("name") - OpenAPI.validate_param(name, "Category", :pattern, val, r"^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$") - end - - if name === Symbol("id") - OpenAPI.validate_param(name, "Category", :format, val, "int64") - end -end diff --git a/test/client/openapigenerator_petstore_v3/petstore/src/models/model_Order.jl b/test/client/openapigenerator_petstore_v3/petstore/src/models/model_Order.jl deleted file mode 100644 index da6c81e..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/src/models/model_Order.jl +++ /dev/null @@ -1,78 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Order -An order for a pets from the pet store - - Order(; - petId=nothing, - shipDate=nothing, - status=nothing, - id=nothing, - complete=false, - quantity=nothing, - ) - - - petId::Int64 - - shipDate::ZonedDateTime - - status::String : Order Status - - id::Int64 - - complete::Bool - - quantity::Int64 -""" -Base.@kwdef mutable struct Order <: OpenAPI.APIModel - petId::Union{Nothing, Int64} = nothing - shipDate::Union{Nothing, ZonedDateTime} = nothing - status::Union{Nothing, String} = nothing - id::Union{Nothing, Int64} = nothing - complete::Union{Nothing, Bool} = false - quantity::Union{Nothing, Int64} = nothing - - function Order(petId, shipDate, status, id, complete, quantity, ) - o = new(petId, shipDate, status, id, complete, quantity, ) - OpenAPI.validate_properties(o) - return o - end -end # type Order - -const _property_types_Order = Dict{Symbol,String}(Symbol("petId")=>"Int64", Symbol("shipDate")=>"ZonedDateTime", Symbol("status")=>"String", Symbol("id")=>"Int64", Symbol("complete")=>"Bool", Symbol("quantity")=>"Int64", ) -OpenAPI.property_type(::Type{ Order }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Order[name]))} - -function OpenAPI.check_required(o::Order) - true -end - -function OpenAPI.validate_properties(o::Order) - OpenAPI.validate_property(Order, Symbol("petId"), o.petId) - OpenAPI.validate_property(Order, Symbol("shipDate"), o.shipDate) - OpenAPI.validate_property(Order, Symbol("status"), o.status) - OpenAPI.validate_property(Order, Symbol("id"), o.id) - OpenAPI.validate_property(Order, Symbol("complete"), o.complete) - OpenAPI.validate_property(Order, Symbol("quantity"), o.quantity) -end - -function OpenAPI.validate_property(::Type{ Order }, name::Symbol, val) - - if name === Symbol("petId") - OpenAPI.validate_param(name, "Order", :format, val, "int64") - end - - if name === Symbol("shipDate") - OpenAPI.validate_param(name, "Order", :format, val, "date-time") - end - - if name === Symbol("status") - OpenAPI.validate_param(name, "Order", :enum, val, ["placed", "approved", "delivered"]) - end - - - if name === Symbol("id") - OpenAPI.validate_param(name, "Order", :format, val, "int64") - end - - - if name === Symbol("quantity") - OpenAPI.validate_param(name, "Order", :format, val, "int32") - end -end diff --git a/test/client/openapigenerator_petstore_v3/petstore/src/models/model_Pet.jl b/test/client/openapigenerator_petstore_v3/petstore/src/models/model_Pet.jl deleted file mode 100644 index 9b30f64..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/src/models/model_Pet.jl +++ /dev/null @@ -1,71 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Pet -A pet for sale in the pet store - - Pet(; - name=nothing, - status=nothing, - id=nothing, - photoUrls=nothing, - tags=nothing, - category=nothing, - ) - - - name::String - - status::String : pet status in the store - - id::Int64 - - photoUrls::Vector{String} - - tags::Vector{Tag} - - category::Category -""" -Base.@kwdef mutable struct Pet <: OpenAPI.APIModel - name::Union{Nothing, String} = nothing - status::Union{Nothing, String} = nothing - id::Union{Nothing, Int64} = nothing - photoUrls::Union{Nothing, Vector{String}} = nothing - tags::Union{Nothing, Vector} = nothing # spec type: Union{ Nothing, Vector{Tag} } - category = nothing # spec type: Union{ Nothing, Category } - - function Pet(name, status, id, photoUrls, tags, category, ) - o = new(name, status, id, photoUrls, tags, category, ) - OpenAPI.validate_properties(o) - return o - end -end # type Pet - -const _property_types_Pet = Dict{Symbol,String}(Symbol("name")=>"String", Symbol("status")=>"String", Symbol("id")=>"Int64", Symbol("photoUrls")=>"Vector{String}", Symbol("tags")=>"Vector{Tag}", Symbol("category")=>"Category", ) -OpenAPI.property_type(::Type{ Pet }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Pet[name]))} - -function OpenAPI.check_required(o::Pet) - o.name === nothing && (return false) - o.photoUrls === nothing && (return false) - true -end - -function OpenAPI.validate_properties(o::Pet) - OpenAPI.validate_property(Pet, Symbol("name"), o.name) - OpenAPI.validate_property(Pet, Symbol("status"), o.status) - OpenAPI.validate_property(Pet, Symbol("id"), o.id) - OpenAPI.validate_property(Pet, Symbol("photoUrls"), o.photoUrls) - OpenAPI.validate_property(Pet, Symbol("tags"), o.tags) - OpenAPI.validate_property(Pet, Symbol("category"), o.category) -end - -function OpenAPI.validate_property(::Type{ Pet }, name::Symbol, val) - - - if name === Symbol("status") - OpenAPI.validate_param(name, "Pet", :enum, val, ["available", "pending", "sold"]) - end - - - if name === Symbol("id") - OpenAPI.validate_param(name, "Pet", :format, val, "int64") - end - - - -end diff --git a/test/client/openapigenerator_petstore_v3/petstore/src/models/model_Tag.jl b/test/client/openapigenerator_petstore_v3/petstore/src/models/model_Tag.jl deleted file mode 100644 index 0550acc..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/src/models/model_Tag.jl +++ /dev/null @@ -1,45 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Tag -A tag for a pet - - Tag(; - name=nothing, - id=nothing, - ) - - - name::String - - id::Int64 -""" -Base.@kwdef mutable struct Tag <: OpenAPI.APIModel - name::Union{Nothing, String} = nothing - id::Union{Nothing, Int64} = nothing - - function Tag(name, id, ) - o = new(name, id, ) - OpenAPI.validate_properties(o) - return o - end -end # type Tag - -const _property_types_Tag = Dict{Symbol,String}(Symbol("name")=>"String", Symbol("id")=>"Int64", ) -OpenAPI.property_type(::Type{ Tag }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Tag[name]))} - -function OpenAPI.check_required(o::Tag) - true -end - -function OpenAPI.validate_properties(o::Tag) - OpenAPI.validate_property(Tag, Symbol("name"), o.name) - OpenAPI.validate_property(Tag, Symbol("id"), o.id) -end - -function OpenAPI.validate_property(::Type{ Tag }, name::Symbol, val) - - - if name === Symbol("id") - OpenAPI.validate_param(name, "Tag", :format, val, "int64") - end -end diff --git a/test/client/openapigenerator_petstore_v3/petstore/src/models/model_User.jl b/test/client/openapigenerator_petstore_v3/petstore/src/models/model_User.jl deleted file mode 100644 index 95e836a..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore/src/models/model_User.jl +++ /dev/null @@ -1,78 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""User -A User who is purchasing from the pet store - - User(; - password=nothing, - id=nothing, - username=nothing, - firstName=nothing, - lastName=nothing, - phone=nothing, - userStatus=nothing, - email=nothing, - ) - - - password::String - - id::Int64 - - username::String - - firstName::String - - lastName::String - - phone::String - - userStatus::Int64 : User Status - - email::String -""" -Base.@kwdef mutable struct User <: OpenAPI.APIModel - password::Union{Nothing, String} = nothing - id::Union{Nothing, Int64} = nothing - username::Union{Nothing, String} = nothing - firstName::Union{Nothing, String} = nothing - lastName::Union{Nothing, String} = nothing - phone::Union{Nothing, String} = nothing - userStatus::Union{Nothing, Int64} = nothing - email::Union{Nothing, String} = nothing - - function User(password, id, username, firstName, lastName, phone, userStatus, email, ) - o = new(password, id, username, firstName, lastName, phone, userStatus, email, ) - OpenAPI.validate_properties(o) - return o - end -end # type User - -const _property_types_User = Dict{Symbol,String}(Symbol("password")=>"String", Symbol("id")=>"Int64", Symbol("username")=>"String", Symbol("firstName")=>"String", Symbol("lastName")=>"String", Symbol("phone")=>"String", Symbol("userStatus")=>"Int64", Symbol("email")=>"String", ) -OpenAPI.property_type(::Type{ User }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_User[name]))} - -function OpenAPI.check_required(o::User) - true -end - -function OpenAPI.validate_properties(o::User) - OpenAPI.validate_property(User, Symbol("password"), o.password) - OpenAPI.validate_property(User, Symbol("id"), o.id) - OpenAPI.validate_property(User, Symbol("username"), o.username) - OpenAPI.validate_property(User, Symbol("firstName"), o.firstName) - OpenAPI.validate_property(User, Symbol("lastName"), o.lastName) - OpenAPI.validate_property(User, Symbol("phone"), o.phone) - OpenAPI.validate_property(User, Symbol("userStatus"), o.userStatus) - OpenAPI.validate_property(User, Symbol("email"), o.email) -end - -function OpenAPI.validate_property(::Type{ User }, name::Symbol, val) - - - if name === Symbol("id") - OpenAPI.validate_param(name, "User", :format, val, "int64") - end - - - - - - if name === Symbol("userStatus") - OpenAPI.validate_param(name, "User", :format, val, "int32") - end - -end diff --git a/test/client/openapigenerator_petstore_v3/petstore_test_petapi.jl b/test/client/openapigenerator_petstore_v3/petstore_test_petapi.jl deleted file mode 100644 index 7db5dd9..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore_test_petapi.jl +++ /dev/null @@ -1,77 +0,0 @@ -module TestPetApi - -using ..OpenAPIGenPetStoreClient -using Test -using OpenAPI -using OpenAPI.Clients -import OpenAPI.Clients: Client - -function test(uri, httplib; test_file_upload=false) - @info("PetApi") - client = Client(uri; httplib=httplib) - api = PetApi(client) - - tag1 = Tag(;id=10, name="juliacat") - tag2 = Tag(;id=11, name="white") - cat = Category(;id=10, name="cat") - - @test_throws OpenAPI.ValidationException Pet(;id=10, category=cat, name="felix", photoUrls=nothing, tags=[tag1, tag2], status="invalid-status") - - pet = Pet(;id=10, category=cat, name="felix", photoUrls=["http://photo/1","http://photo/2"], tags=[tag1,tag2], status="pending") - - @info("PetApi - add_pet") - api_return, http_resp = add_pet(api, pet) - @test isa(api_return, Pet) - @test http_resp.status == 200 - - @info("PetApi - update_pet") - pet.status = "available" - api_return, http_resp = update_pet(api, pet) - @test isa(api_return, Pet) - @test http_resp.status == 200 - - # @info("PetApi - update_pet_with_form") - # @test update_pet_with_form(api, 10; in_name="meow") === nothing - - @info("PetApi - get_pet_by_id") - pet10, http_resp = get_pet_by_id(api, Int64(10)) - @test pet10.id == 10 - @test http_resp.status == 200 - - @info("PetApi - find_pets_by_status") - unsold = ["available", "pending"] - pets, http_resp = find_pets_by_status(api, unsold) - @test isa(pets, Vector{Pet}) - @test http_resp.status == 200 - @info("PetApi - find_pets_by_status", npets=length(pets)) - for p in pets - @test p.status in unsold - end - - @info("PetApi - delete_pet") - api_return, http_resp = delete_pet(api, Int64(10)) - @test api_return === nothing - @test http_resp.status == 200 - - if test_file_upload - @info("PetApi - upload_file") - api_return, http_resp = upload_file(api, 1; additional_metadata="my metadata", file=@__FILE__) - @test isa(api_return, ApiResponse) - @test api_return.code == 1 - @test api_return.type == "pet" - @test api_return.message == "file uploaded" - @test http_resp.status == 200 - end - - # does not work yet. issue: https://github.com/JuliaWeb/Requests.jl/issues/139 - #@info("PetApi - upload_file") - #img = joinpath(dirname(@__FILE__), "cat.png") - #resp, http_resp = upload_file(api, 10; additionalMetadata="juliacat pic", file=img) - #@test isa(resp, ApiResponse) - #@test resp.code == 200 - #@info("PetApi - upload_file", typ=get_field(resp, "type"), message=get_field(resp, "message")) - - nothing -end - -end # module TestPetApi diff --git a/test/client/openapigenerator_petstore_v3/petstore_test_storeapi.jl b/test/client/openapigenerator_petstore_v3/petstore_test_storeapi.jl deleted file mode 100644 index 59b3a83..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore_test_storeapi.jl +++ /dev/null @@ -1,74 +0,0 @@ -module TestStoreApi - -using ..OpenAPIGenPetStoreClient -using Test -using Dates -using TimeZones -using OpenAPI -using OpenAPI.Clients -import OpenAPI.Clients: Client - -function test(uri, httplib::Symbol) - @info("StoreApi") - client = Client(uri; httplib=httplib) - api = StoreApi(client) - - @info("StoreApi - get_inventory") - inventory, http_resp = get_inventory(api) - @test http_resp.status == 200 - @test isa(inventory, Dict{String,Int64}) - @test !isempty(inventory) - - @info("StoreApi - place_order") - @test_throws OpenAPI.ValidationException Order(; id=5, petId=10, quantity=2, shipDate=ZonedDateTime(DateTime(2017, 03, 12), localzone()), status="invalid_status", complete=false) - order = Order(; id=5, petId=10, quantity=2, shipDate=ZonedDateTime(DateTime(2017, 03, 12), localzone()), status="placed", complete=false) - neworder, http_resp = place_order(api, order) - @test http_resp.status == 200 - @test neworder.id == 5 - - @info("StoreApi - get_order_by_id") - @test_throws OpenAPI.ValidationException get_order_by_id(api, Int64(0)) - order, http_resp = get_order_by_id(api, Int64(5)) - @test http_resp.status == 200 - @test isa(order, Order) - @test order.id == 5 - @test isa(order.shipDate, ZonedDateTime) - - @info("StoreApi - get_order_by_id (async)") - response_channel = Channel{Order}(1) - @test_throws OpenAPI.ValidationException get_order_by_id(api, response_channel, Int64(0)) - @sync begin - @async begin - api_return, http_resp = get_order_by_id(api, response_channel, Int64(5)) - @test (200 <= http_resp.status <= 206) - @test api_return === response_channel - end - @async begin - order = take!(response_channel) - @test isa(order, Order) - @test order.id == 5 - end - end - - # a closed channel is equivalent of cancellation of the call, - # no error should be thrown, but response can be nothing if call was interrupted immediately - @test !isopen(response_channel) - - # open a new channel to use - response_channel = Channel{Order}(1) - try - resp, http_resp = get_order_by_id(api, response_channel, Int64(5)) - @test (200 <= http_resp.status <= 206) - catch ex - @test isa(ex, OpenAPI.InvocationException) - end - - @info("StoreApi - delete_order") - api_return, http_resp = delete_order(api, "5") - @test api_return === nothing - @test http_resp.status == 200 - - nothing -end - -end # module TestStoreApi diff --git a/test/client/openapigenerator_petstore_v3/petstore_test_userapi.jl b/test/client/openapigenerator_petstore_v3/petstore_test_userapi.jl deleted file mode 100644 index 689ead1..0000000 --- a/test/client/openapigenerator_petstore_v3/petstore_test_userapi.jl +++ /dev/null @@ -1,193 +0,0 @@ -module TestUserApi - -using ..OpenAPIGenPetStoreClient -using Test -using Random -using JSON -using URIs -using OpenAPI -using OpenAPI.Clients -import OpenAPI.Clients: Client, Ctx, ApiException, DEFAULT_TIMEOUT_SECS, with_timeout, set_timeout, set_user_agent, set_cookie - -const TEST_USER = "jloac" -const TEST_USER1 = "jloac1" -const TEST_USER2 = "jloac2" -const TEST_USER3 = "jl oac 3" -const PRESET_TEST_USER = "user1" # this is the username that works for get user requests (as documented in the test docker container API) - -function test_404(uri) - @info("Error handling") - client = Client(uri*"/invalid") - api = UserApi(client) - - api_return, http_resp = login_user(api, TEST_USER, "testpassword") - @test api_return === nothing - @test http_resp.status == 404 - - client = Client("http://_invalid/") - api = UserApi(client) - - try - login_user(api, TEST_USER, "testpassword") - @error("ApiException not thrown") - catch ex - @test isa(ex, ApiException) - @test startswith(ex.reason, "Could not resolve host") || startswith(ex.reason, "DNSError") - end -end - -function test_set_methods() - @info("Error handling") - client = Client("http://_invalid/") - - @test client.timeout[] == DEFAULT_TIMEOUT_SECS - - with_timeout(client, DEFAULT_TIMEOUT_SECS + 10) do client - @test client.timeout[] == DEFAULT_TIMEOUT_SECS + 10 - end - @test client.timeout[] == DEFAULT_TIMEOUT_SECS - - api = UserApi(client) - with_timeout(api, DEFAULT_TIMEOUT_SECS + 10) do api - @test api.client.timeout[] == DEFAULT_TIMEOUT_SECS + 10 - end - @test client.timeout[] == DEFAULT_TIMEOUT_SECS - - set_timeout(client, DEFAULT_TIMEOUT_SECS + 10) - @test client.timeout[] == DEFAULT_TIMEOUT_SECS + 10 - - @test isempty(client.headers) - set_user_agent(client, "007") - set_cookie(client, "crumbly") - @test client.headers["User-Agent"] == "007" - @test client.headers["Cookie"] == "crumbly" -end - -function test_login_user_hook(ctx::Ctx) - ctx.header["actual_password"] = "testpassword" - ctx -end - -function test_login_user_hook(resource_path::AbstractString, body::Any, headers::Dict{String,String}) - uri = URIs.parse_uri(resource_path) - qparams = URIs.queryparams(uri) - qparams["password"] = headers["actual_password"] - delete!(headers, "actual_password") - resource_path = string(URIs.URI(uri; query=escapeuri(qparams))) - - (resource_path, body, headers) -end - -function test_userhook(uri) - @info("User hook") - client = Client(uri; pre_request_hook=test_login_user_hook) - api = UserApi(client) - - login_result, http_resp = login_user(api, TEST_USER, "wrongpassword") - @test http_resp.status == 200 - @test !isempty(login_result) - @test startswith(login_result, "logged in user session:") -end - -function test_parallel(uri) - @info("Parallel usage") - client = Client(uri) - api = UserApi(client) - - for gcidx in 1:100 - @sync begin - for idx in 1:10^3 - @async begin - @debug("[$idx] UserApi Parallel begin") - login_result, http_resp = login_user(api, TEST_USER, "testpassword") - @test http_resp.status == 200 - @test !isempty(login_result) - @test startswith(login_result, "logged in user session:") - - @test_throws ApiException get_user_by_name(api, randstring()) - @test_throws ApiException get_user_by_name(api, TEST_USER) - - logout_result, http_resp = logout_user(api) - @test http_resp.status == 200 - @test logout_result === nothing - @debug("[$idx] UserApi Parallel end") - end - end - end - GC.gc() - @info("outer loop $gcidx") - end - nothing -end - -function test(uri, httplib::Symbol) - @info("UserApi") - client = Client(uri; httplib=httplib) - api = UserApi(client) - - @info("UserApi - login_user") - login_result, http_resp = login_user(api, TEST_USER, "testpassword") - @test http_resp.status == 200 - @test !isempty(login_result) - - @info("UserApi - create_user") - user1 = User(; id=100, username=TEST_USER1, firstName="test1", lastName="user1", email="jloac1@example.com", password="testpass1", phone="1000000001", userStatus=0) - create_result, http_resp = create_user(api, user1) - @test http_resp.status == 200 - @test create_result === nothing - - @info("UserApi - create_users_with_array_input") - user2 = User(; id=200, username=TEST_USER2, firstName="test2", lastName="user2", email="jloac2@example.com", password="testpass2", phone="1000000002", userStatus=0) - create_result, http_resp = create_users_with_array_input(api, [user1, user2]) - @test http_resp.status == 200 - @test create_result === nothing - - @info("UserApi - create_users_with_array_input") - create_result, http_resp = create_users_with_array_input(api, [user1, user2]) - @test http_resp.status == 200 - @test create_result === nothing - - @info("UserApi - get_user_by_name") - getuser_result, http_resp = get_user_by_name(api, randstring()) - @test http_resp.status == 404 - @test nothing === getuser_result - getuser_result, http_resp = get_user_by_name(api, TEST_USER) - @test http_resp.status == 404 - @test nothing === getuser_result - getuser_result, http_resp = get_user_by_name(api, PRESET_TEST_USER) - @test http_resp.status == 200 - @test isa(getuser_result, User) - - @info("UserApi - update_user") - api_return, http_resp = update_user(api, TEST_USER2, getuser_result) - @test http_resp.status == 200 - @test api_return === nothing - @info("UserApi - delete_user") - api_return, http_resp = delete_user(api, TEST_USER2) - @test http_resp.status == 200 - @test api_return === nothing - - @info("UserApi - logout_user") - logout_result, http_resp = logout_user(api) - @test http_resp.status == 200 - @test logout_result === nothing - - @info("UserApi - Test with spaces in username") - user3 = User(; id=300, username=TEST_USER3, firstName="test3", lastName="user3", email="jloac3@example.com", password="testpass3", phone="1000000003", userStatus=0) - create_result, http_resp = create_user(api, user3) - @test http_resp.status == 200 - @test create_result === nothing - - user3.firstName = "test3 updated" - api_return, http_resp = update_user(api, TEST_USER3, user3) - @test http_resp.status == 200 - @test api_return === nothing - - api_return, http_resp = delete_user(api, TEST_USER3) - @test http_resp.status == 200 - @test api_return === nothing - - nothing -end - -end # module TestUserApi diff --git a/test/client/openapigenerator_petstore_v3/runtests.jl b/test/client/openapigenerator_petstore_v3/runtests.jl deleted file mode 100644 index 8e63f5d..0000000 --- a/test/client/openapigenerator_petstore_v3/runtests.jl +++ /dev/null @@ -1,20 +0,0 @@ -module OpenAPIGenPetStoreV3Tests - -include(joinpath(@__DIR__, "petstore", "src", "OpenAPIGenPetStoreClient.jl")) -using .OpenAPIGenPetStoreClient -using Test - -include("petstore_test_petapi.jl") -include("petstore_test_userapi.jl") -include("petstore_test_storeapi.jl") - -const server = "http://127.0.0.1:8081/v3" - -function runtests(httplib::Symbol; test_file_upload=false) - @testset "petstore v3" begin - TestUserApi.test(server, httplib) - TestStoreApi.test(server, httplib) - TestPetApi.test(server, httplib; test_file_upload=test_file_upload) - end -end -end # module OpenAPIGenPetStoreV3Tests diff --git a/test/client/param_serialize.jl b/test/client/param_serialize.jl deleted file mode 100644 index c6630aa..0000000 --- a/test/client/param_serialize.jl +++ /dev/null @@ -1,45 +0,0 @@ -using OpenAPI.Clients: deep_object_serialize - -@testset "Test deep_object_serialize" begin - @testset "Single level object" begin - dict = Dict("key1" => "value1", "key2" => "value2") - expected = Dict("key1" => "value1", "key2" => "value2") - @test deep_object_serialize(dict) == expected - end - - @testset "Nested object" begin - dict = Dict("outer" => Dict("inner" => "value")) - expected = Dict("outer[inner]" => "value") - @test deep_object_serialize(dict) == expected - end - - @testset "Deeply nested object" begin - dict = Dict("a" => Dict("b" => Dict("c" => Dict("d" => "value")))) - expected = Dict("a[b][c][d]" => "value") - @test deep_object_serialize(dict) == expected - end - - @testset "Multiple nested objects" begin - dict = Dict("a" => Dict("b" => "value1", "c" => "value2")) - expected = Dict("a[b]" => "value1", "a[c]" => "value2") - @test deep_object_serialize(dict) == expected - end - - @testset "Dictionary represented array" begin - dict = Dict("a" => ["value1", "value2"]) - expected = Dict("a[0]" => "value1", "a[1]" => "value2") - @test deep_object_serialize(dict) == expected - end - - @testset "Mixed structure" begin - dict = Dict("a" => Dict("b" => "value1", "c" => ["value2", "value3"])) - expected = Dict("a[b]" => "value1", "a[c][0]" => "value2", "a[c][1]" => "value3") - @test deep_object_serialize(dict) == expected - end - - @testset "Blank values" begin - dict = Dict("a" => Dict("b" => "", "c" => "")) - expected = Dict("a[b]" => "", "a[c]" => "") - @test deep_object_serialize(dict) == expected - end -end diff --git a/test/client/petstore_v2/generate.sh b/test/client/petstore_v2/generate.sh deleted file mode 100755 index d179ed4..0000000 --- a/test/client/petstore_v2/generate.sh +++ /dev/null @@ -1,7 +0,0 @@ -java -jar openapi-generator-cli.jar generate \ - -i ../../specs/petstore_v2.json \ - -g julia-client \ - -o petstore \ - --additional-properties=packageName=PetStoreClient \ - --additional-properties=exportModels=true \ - --additional-properties=exportOperations=true diff --git a/test/client/petstore_v2/petstore/.openapi-generator-ignore b/test/client/petstore_v2/petstore/.openapi-generator-ignore deleted file mode 100644 index 7484ee5..0000000 --- a/test/client/petstore_v2/petstore/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/test/client/petstore_v2/petstore/.openapi-generator/FILES b/test/client/petstore_v2/petstore/.openapi-generator/FILES deleted file mode 100644 index b9cea9e..0000000 --- a/test/client/petstore_v2/petstore/.openapi-generator/FILES +++ /dev/null @@ -1,21 +0,0 @@ -README.md -docs/ApiResponse.md -docs/Category.md -docs/Order.md -docs/Pet.md -docs/PetApi.md -docs/StoreApi.md -docs/Tag.md -docs/User.md -docs/UserApi.md -src/PetStoreClient.jl -src/apis/api_PetApi.jl -src/apis/api_StoreApi.jl -src/apis/api_UserApi.jl -src/modelincludes.jl -src/models/model_ApiResponse.jl -src/models/model_Category.jl -src/models/model_Order.jl -src/models/model_Pet.jl -src/models/model_Tag.jl -src/models/model_User.jl diff --git a/test/client/petstore_v2/petstore/.openapi-generator/VERSION b/test/client/petstore_v2/petstore/.openapi-generator/VERSION deleted file mode 100644 index 96cfbb1..0000000 --- a/test/client/petstore_v2/petstore/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.13.0-SNAPSHOT diff --git a/test/client/petstore_v2/petstore/README.md b/test/client/petstore_v2/petstore/README.md deleted file mode 100644 index ffb43f5..0000000 --- a/test/client/petstore_v2/petstore/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# Julia API client for PetStoreClient - -This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. - -## Overview -This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. - -- API version: 1.0.6 -- Generator version: 7.13.0-SNAPSHOT -- Build package: org.openapitools.codegen.languages.JuliaClientCodegen - - -## Installation -Place the Julia files generated under the `src` folder in your Julia project. Include PetStoreClient.jl in the project code. -It would include the module named PetStoreClient. - -Documentation is generated as markdown files under the `docs` folder. You can include them in your project documentation. -Documentation is also embedded in Julia which can be used with a Julia specific documentation generator. - -## API Endpoints - -Class | Method ------------- | ------------- -*PetApi* | [**add_pet**](docs/PetApi.md#add_pet)
**POST** /pet
Add a new pet to the store -*PetApi* | [**delete_pet**](docs/PetApi.md#delete_pet)
**DELETE** /pet/{petId}
Deletes a pet -*PetApi* | [**find_pets_by_status**](docs/PetApi.md#find_pets_by_status)
**GET** /pet/findByStatus
Finds Pets by status -*PetApi* | [**find_pets_by_tags**](docs/PetApi.md#find_pets_by_tags)
**GET** /pet/findByTags
Finds Pets by tags -*PetApi* | [**get_pet_by_id**](docs/PetApi.md#get_pet_by_id)
**GET** /pet/{petId}
Find pet by ID -*PetApi* | [**update_pet**](docs/PetApi.md#update_pet)
**PUT** /pet
Update an existing pet -*PetApi* | [**update_pet_with_form**](docs/PetApi.md#update_pet_with_form)
**POST** /pet/{petId}
Updates a pet in the store with form data -*PetApi* | [**upload_file**](docs/PetApi.md#upload_file)
**POST** /pet/{petId}/uploadImage
uploads an image -*StoreApi* | [**delete_order**](docs/StoreApi.md#delete_order)
**DELETE** /store/order/{orderId}
Delete purchase order by ID -*StoreApi* | [**get_inventory**](docs/StoreApi.md#get_inventory)
**GET** /store/inventory
Returns pet inventories by status -*StoreApi* | [**get_order_by_id**](docs/StoreApi.md#get_order_by_id)
**GET** /store/order/{orderId}
Find purchase order by ID -*StoreApi* | [**place_order**](docs/StoreApi.md#place_order)
**POST** /store/order
Place an order for a pet -*UserApi* | [**create_user**](docs/UserApi.md#create_user)
**POST** /user
Create user -*UserApi* | [**create_users_with_array_input**](docs/UserApi.md#create_users_with_array_input)
**POST** /user/createWithArray
Creates list of users with given input array -*UserApi* | [**create_users_with_list_input**](docs/UserApi.md#create_users_with_list_input)
**POST** /user/createWithList
Creates list of users with given input array -*UserApi* | [**delete_user**](docs/UserApi.md#delete_user)
**DELETE** /user/{username}
Delete user -*UserApi* | [**get_user_by_name**](docs/UserApi.md#get_user_by_name)
**GET** /user/{username}
Get user by user name -*UserApi* | [**login_user**](docs/UserApi.md#login_user)
**GET** /user/login
Logs user into the system -*UserApi* | [**logout_user**](docs/UserApi.md#logout_user)
**GET** /user/logout
Logs out current logged in user session -*UserApi* | [**update_user**](docs/UserApi.md#update_user)
**PUT** /user/{username}
Updated user - - -## Models - - - [ApiResponse](docs/ApiResponse.md) - - [Category](docs/Category.md) - - [Order](docs/Order.md) - - [Pet](docs/Pet.md) - - [Tag](docs/Tag.md) - - [User](docs/User.md) - - - -## Authorization - -Authentication schemes defined for the API: - -### api_key -- **Type**: API key - -Example -``` - using OpenAPI - using OpenAPI.Clients - import OpenAPI.Clients: Client - client = Client(server_uri) - api = MyApi(client) - result = callApi(api, args...; api_key) -``` - -### petstore_auth -- **Type**: OAuth -- **Flow**: implicit -- **Authorization URL**: https://petstore.swagger.io/oauth/authorize -- **Scopes**: - - **read:pets**: read your pets - - **write:pets**: modify pets in your account - -Example -``` - using OpenAPI - using OpenAPI.Clients - import OpenAPI.Clients: Client, set_header - client = Client(server_uri) - set_header(client, "Authorization", "Bearer $bearer_auth") - api = MyApi(client) - result = callApi(api, args...; api_key) -``` - -## Author - -apiteam@swagger.io - diff --git a/test/client/petstore_v2/petstore/docs/ApiResponse.md b/test/client/petstore_v2/petstore/docs/ApiResponse.md deleted file mode 100644 index 664dd24..0000000 --- a/test/client/petstore_v2/petstore/docs/ApiResponse.md +++ /dev/null @@ -1,14 +0,0 @@ -# ApiResponse - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**code** | **Int64** | | [optional] [default to nothing] -**type** | **String** | | [optional] [default to nothing] -**message** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/petstore_v2/petstore/docs/Category.md b/test/client/petstore_v2/petstore/docs/Category.md deleted file mode 100644 index 4e93290..0000000 --- a/test/client/petstore_v2/petstore/docs/Category.md +++ /dev/null @@ -1,13 +0,0 @@ -# Category - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**name** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/petstore_v2/petstore/docs/Order.md b/test/client/petstore_v2/petstore/docs/Order.md deleted file mode 100644 index d94aa08..0000000 --- a/test/client/petstore_v2/petstore/docs/Order.md +++ /dev/null @@ -1,17 +0,0 @@ -# Order - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**petId** | **Int64** | | [optional] [default to nothing] -**quantity** | **Int64** | | [optional] [default to nothing] -**shipDate** | **ZonedDateTime** | | [optional] [default to nothing] -**status** | **String** | Order Status | [optional] [default to nothing] -**complete** | **Bool** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/petstore_v2/petstore/docs/Pet.md b/test/client/petstore_v2/petstore/docs/Pet.md deleted file mode 100644 index a8bba4c..0000000 --- a/test/client/petstore_v2/petstore/docs/Pet.md +++ /dev/null @@ -1,17 +0,0 @@ -# Pet - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**category** | [***Category**](Category.md) | | [optional] [default to nothing] -**name** | **String** | | [default to nothing] -**photoUrls** | **Vector{String}** | | [default to nothing] -**tags** | [**Vector{Tag}**](Tag.md) | | [optional] [default to nothing] -**status** | **String** | pet status in the store | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/petstore_v2/petstore/docs/PetApi.md b/test/client/petstore_v2/petstore/docs/PetApi.md deleted file mode 100644 index 2d1454d..0000000 --- a/test/client/petstore_v2/petstore/docs/PetApi.md +++ /dev/null @@ -1,266 +0,0 @@ -# PetApi - -All URIs are relative to *https://petstore.swagger.io/v2* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**add_pet**](PetApi.md#add_pet) | **POST** /pet | Add a new pet to the store -[**delete_pet**](PetApi.md#delete_pet) | **DELETE** /pet/{petId} | Deletes a pet -[**find_pets_by_status**](PetApi.md#find_pets_by_status) | **GET** /pet/findByStatus | Finds Pets by status -[**find_pets_by_tags**](PetApi.md#find_pets_by_tags) | **GET** /pet/findByTags | Finds Pets by tags -[**get_pet_by_id**](PetApi.md#get_pet_by_id) | **GET** /pet/{petId} | Find pet by ID -[**update_pet**](PetApi.md#update_pet) | **PUT** /pet | Update an existing pet -[**update_pet_with_form**](PetApi.md#update_pet_with_form) | **POST** /pet/{petId} | Updates a pet in the store with form data -[**upload_file**](PetApi.md#upload_file) | **POST** /pet/{petId}/uploadImage | uploads an image - - -# **add_pet** -> add_pet(_api::PetApi, body::Pet; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> add_pet(_api::PetApi, response_stream::Channel, body::Pet; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Add a new pet to the store - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**body** | [**Pet**](Pet.md) | Pet object that needs to be added to the store | - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: application/json, application/xml - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **delete_pet** -> delete_pet(_api::PetApi, pet_id::Int64; api_key=nothing, _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> delete_pet(_api::PetApi, response_stream::Channel, pet_id::Int64; api_key=nothing, _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Deletes a pet - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**pet_id** | **Int64** | Pet id to delete | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **api_key** | **String** | | [default to nothing] - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **find_pets_by_status** -> find_pets_by_status(_api::PetApi, status::Vector{String}; _mediaType=nothing) -> Vector{Pet}, OpenAPI.Clients.ApiResponse
-> find_pets_by_status(_api::PetApi, response_stream::Channel, status::Vector{String}; _mediaType=nothing) -> Channel{ Vector{Pet} }, OpenAPI.Clients.ApiResponse - -Finds Pets by status - -Multiple status values can be provided with comma separated strings - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**status** | [**Vector{String}**](String.md) | Status values that need to be considered for filter | - -### Return type - -[**Vector{Pet}**](Pet.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json, application/xml - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **find_pets_by_tags** -> find_pets_by_tags(_api::PetApi, tags::Vector{String}; _mediaType=nothing) -> Vector{Pet}, OpenAPI.Clients.ApiResponse
-> find_pets_by_tags(_api::PetApi, response_stream::Channel, tags::Vector{String}; _mediaType=nothing) -> Channel{ Vector{Pet} }, OpenAPI.Clients.ApiResponse - -Finds Pets by tags - -Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**tags** | [**Vector{String}**](String.md) | Tags to filter by | - -### Return type - -[**Vector{Pet}**](Pet.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json, application/xml - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **get_pet_by_id** -> get_pet_by_id(_api::PetApi, pet_id::Int64; _mediaType=nothing) -> Pet, OpenAPI.Clients.ApiResponse
-> get_pet_by_id(_api::PetApi, response_stream::Channel, pet_id::Int64; _mediaType=nothing) -> Channel{ Pet }, OpenAPI.Clients.ApiResponse - -Find pet by ID - -Returns a single pet - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**pet_id** | **Int64** | ID of pet to return | - -### Return type - -[**Pet**](Pet.md) - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json, application/xml - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **update_pet** -> update_pet(_api::PetApi, body::Pet; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> update_pet(_api::PetApi, response_stream::Channel, body::Pet; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Update an existing pet - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**body** | [**Pet**](Pet.md) | Pet object that needs to be added to the store | - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: application/json, application/xml - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **update_pet_with_form** -> update_pet_with_form(_api::PetApi, pet_id::Int64; name=nothing, status=nothing, _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> update_pet_with_form(_api::PetApi, response_stream::Channel, pet_id::Int64; name=nothing, status=nothing, _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Updates a pet in the store with form data - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**pet_id** | **Int64** | ID of pet that needs to be updated | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **name** | **String** | Updated name of the pet | [default to nothing] - **status** | **String** | Updated status of the pet | [default to nothing] - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: application/x-www-form-urlencoded - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **upload_file** -> upload_file(_api::PetApi, pet_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) -> ApiResponse, OpenAPI.Clients.ApiResponse
-> upload_file(_api::PetApi, response_stream::Channel, pet_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) -> Channel{ ApiResponse }, OpenAPI.Clients.ApiResponse - -uploads an image - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**pet_id** | **Int64** | ID of pet to update | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **additional_metadata** | **String** | Additional data to pass to server | [default to nothing] - **file** | **String** | file to upload | - -### Return type - -[**ApiResponse**](ApiResponse.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: multipart/form-data - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - diff --git a/test/client/petstore_v2/petstore/docs/StoreApi.md b/test/client/petstore_v2/petstore/docs/StoreApi.md deleted file mode 100644 index 0a1b029..0000000 --- a/test/client/petstore_v2/petstore/docs/StoreApi.md +++ /dev/null @@ -1,126 +0,0 @@ -# StoreApi - -All URIs are relative to *https://petstore.swagger.io/v2* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**delete_order**](StoreApi.md#delete_order) | **DELETE** /store/order/{orderId} | Delete purchase order by ID -[**get_inventory**](StoreApi.md#get_inventory) | **GET** /store/inventory | Returns pet inventories by status -[**get_order_by_id**](StoreApi.md#get_order_by_id) | **GET** /store/order/{orderId} | Find purchase order by ID -[**place_order**](StoreApi.md#place_order) | **POST** /store/order | Place an order for a pet - - -# **delete_order** -> delete_order(_api::StoreApi, order_id::Int64; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> delete_order(_api::StoreApi, response_stream::Channel, order_id::Int64; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Delete purchase order by ID - -For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **StoreApi** | API context | -**order_id** | **Int64** | ID of the order that needs to be deleted | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **get_inventory** -> get_inventory(_api::StoreApi; _mediaType=nothing) -> Dict{String, Int64}, OpenAPI.Clients.ApiResponse
-> get_inventory(_api::StoreApi, response_stream::Channel; _mediaType=nothing) -> Channel{ Dict{String, Int64} }, OpenAPI.Clients.ApiResponse - -Returns pet inventories by status - -Returns a map of status codes to quantities - -### Required Parameters -This endpoint does not need any parameter. - -### Return type - -**Dict{String, Int64}** - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **get_order_by_id** -> get_order_by_id(_api::StoreApi, order_id::Int64; _mediaType=nothing) -> Order, OpenAPI.Clients.ApiResponse
-> get_order_by_id(_api::StoreApi, response_stream::Channel, order_id::Int64; _mediaType=nothing) -> Channel{ Order }, OpenAPI.Clients.ApiResponse - -Find purchase order by ID - -For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **StoreApi** | API context | -**order_id** | **Int64** | ID of pet that needs to be fetched | - -### Return type - -[**Order**](Order.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json, application/xml - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **place_order** -> place_order(_api::StoreApi, body::Order; _mediaType=nothing) -> Order, OpenAPI.Clients.ApiResponse
-> place_order(_api::StoreApi, response_stream::Channel, body::Order; _mediaType=nothing) -> Channel{ Order }, OpenAPI.Clients.ApiResponse - -Place an order for a pet - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **StoreApi** | API context | -**body** | [**Order**](Order.md) | order placed for purchasing the pet | - -### Return type - -[**Order**](Order.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json, application/xml - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - diff --git a/test/client/petstore_v2/petstore/docs/Tag.md b/test/client/petstore_v2/petstore/docs/Tag.md deleted file mode 100644 index c904872..0000000 --- a/test/client/petstore_v2/petstore/docs/Tag.md +++ /dev/null @@ -1,13 +0,0 @@ -# Tag - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**name** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/petstore_v2/petstore/docs/User.md b/test/client/petstore_v2/petstore/docs/User.md deleted file mode 100644 index 5318b5a..0000000 --- a/test/client/petstore_v2/petstore/docs/User.md +++ /dev/null @@ -1,19 +0,0 @@ -# User - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**username** | **String** | | [optional] [default to nothing] -**firstName** | **String** | | [optional] [default to nothing] -**lastName** | **String** | | [optional] [default to nothing] -**email** | **String** | | [optional] [default to nothing] -**password** | **String** | | [optional] [default to nothing] -**phone** | **String** | | [optional] [default to nothing] -**userStatus** | **Int64** | User Status | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/petstore_v2/petstore/docs/UserApi.md b/test/client/petstore_v2/petstore/docs/UserApi.md deleted file mode 100644 index 27639c4..0000000 --- a/test/client/petstore_v2/petstore/docs/UserApi.md +++ /dev/null @@ -1,244 +0,0 @@ -# UserApi - -All URIs are relative to *https://petstore.swagger.io/v2* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**create_user**](UserApi.md#create_user) | **POST** /user | Create user -[**create_users_with_array_input**](UserApi.md#create_users_with_array_input) | **POST** /user/createWithArray | Creates list of users with given input array -[**create_users_with_list_input**](UserApi.md#create_users_with_list_input) | **POST** /user/createWithList | Creates list of users with given input array -[**delete_user**](UserApi.md#delete_user) | **DELETE** /user/{username} | Delete user -[**get_user_by_name**](UserApi.md#get_user_by_name) | **GET** /user/{username} | Get user by user name -[**login_user**](UserApi.md#login_user) | **GET** /user/login | Logs user into the system -[**logout_user**](UserApi.md#logout_user) | **GET** /user/logout | Logs out current logged in user session -[**update_user**](UserApi.md#update_user) | **PUT** /user/{username} | Updated user - - -# **create_user** -> create_user(_api::UserApi, body::User; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> create_user(_api::UserApi, response_stream::Channel, body::User; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Create user - -This can only be done by the logged in user. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**body** | [**User**](User.md) | Created user object | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **create_users_with_array_input** -> create_users_with_array_input(_api::UserApi, body::Vector{User}; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> create_users_with_array_input(_api::UserApi, response_stream::Channel, body::Vector{User}; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Creates list of users with given input array - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**body** | [**Vector{User}**](User.md) | List of user object | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **create_users_with_list_input** -> create_users_with_list_input(_api::UserApi, body::Vector{User}; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> create_users_with_list_input(_api::UserApi, response_stream::Channel, body::Vector{User}; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Creates list of users with given input array - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**body** | [**Vector{User}**](User.md) | List of user object | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **delete_user** -> delete_user(_api::UserApi, username::String; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> delete_user(_api::UserApi, response_stream::Channel, username::String; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Delete user - -This can only be done by the logged in user. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**username** | **String** | The name that needs to be deleted | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **get_user_by_name** -> get_user_by_name(_api::UserApi, username::String; _mediaType=nothing) -> User, OpenAPI.Clients.ApiResponse
-> get_user_by_name(_api::UserApi, response_stream::Channel, username::String; _mediaType=nothing) -> Channel{ User }, OpenAPI.Clients.ApiResponse - -Get user by user name - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**username** | **String** | The name that needs to be fetched. Use user1 for testing. | - -### Return type - -[**User**](User.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json, application/xml - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **login_user** -> login_user(_api::UserApi, username::String, password::String; _mediaType=nothing) -> String, OpenAPI.Clients.ApiResponse
-> login_user(_api::UserApi, response_stream::Channel, username::String, password::String; _mediaType=nothing) -> Channel{ String }, OpenAPI.Clients.ApiResponse - -Logs user into the system - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**username** | **String** | The user name for login | -**password** | **String** | The password for login in clear text | - -### Return type - -**String** - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json, application/xml - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **logout_user** -> logout_user(_api::UserApi; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> logout_user(_api::UserApi, response_stream::Channel; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Logs out current logged in user session - -### Required Parameters -This endpoint does not need any parameter. - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **update_user** -> update_user(_api::UserApi, username::String, body::User; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> update_user(_api::UserApi, response_stream::Channel, username::String, body::User; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Updated user - -This can only be done by the logged in user. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**username** | **String** | name that need to be updated | -**body** | [**User**](User.md) | Updated user object | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - diff --git a/test/client/petstore_v2/petstore/src/PetStoreClient.jl b/test/client/petstore_v2/petstore/src/PetStoreClient.jl deleted file mode 100644 index 8509bcf..0000000 --- a/test/client/petstore_v2/petstore/src/PetStoreClient.jl +++ /dev/null @@ -1,31 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -module PetStoreClient - -using Dates, TimeZones -using OpenAPI -using OpenAPI.Clients - -const API_VERSION = "1.0.6" - -include("modelincludes.jl") - -include("apis/api_PetApi.jl") -include("apis/api_StoreApi.jl") -include("apis/api_UserApi.jl") - -# export models -export ApiResponse -export Category -export Order -export Pet -export Tag -export User - -# export operations -export PetApi -export StoreApi -export UserApi - -end # module PetStoreClient diff --git a/test/client/petstore_v2/petstore/src/apis/api_PetApi.jl b/test/client/petstore_v2/petstore/src/apis/api_PetApi.jl deleted file mode 100644 index 4a3dada..0000000 --- a/test/client/petstore_v2/petstore/src/apis/api_PetApi.jl +++ /dev/null @@ -1,274 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -struct PetApi <: OpenAPI.APIClientImpl - client::OpenAPI.Clients.Client -end - -""" -The default API base path for APIs in `PetApi`. -This can be used to construct the `OpenAPI.Clients.Client` instance. -""" -basepath(::Type{ PetApi }) = "https://petstore.swagger.io/v2" - -const _returntypes_add_pet_PetApi = Dict{Regex,Type}( - Regex("^" * replace("405", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_add_pet(_api::PetApi, body::Pet; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_add_pet_PetApi, "/pet", ["petstore_auth", ], body) - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", "application/xml", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Add a new pet to the store - -Params: -- body::Pet (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function add_pet(_api::PetApi, body::Pet; _mediaType=nothing) - _ctx = _oacinternal_add_pet(_api, body; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function add_pet(_api::PetApi, response_stream::Channel, body::Pet; _mediaType=nothing) - _ctx = _oacinternal_add_pet(_api, body; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_delete_pet_PetApi = Dict{Regex,Type}( - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_delete_pet(_api::PetApi, pet_id::Int64; api_key=nothing, _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "DELETE", _returntypes_delete_pet_PetApi, "/pet/{petId}", ["petstore_auth", ]) - OpenAPI.Clients.set_param(_ctx.path, "petId", pet_id) # type Int64 - OpenAPI.Clients.set_param(_ctx.header, "api_key", api_key) # type String - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Deletes a pet - -Params: -- pet_id::Int64 (required) -- api_key::String - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function delete_pet(_api::PetApi, pet_id::Int64; api_key=nothing, _mediaType=nothing) - _ctx = _oacinternal_delete_pet(_api, pet_id; api_key=api_key, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function delete_pet(_api::PetApi, response_stream::Channel, pet_id::Int64; api_key=nothing, _mediaType=nothing) - _ctx = _oacinternal_delete_pet(_api, pet_id; api_key=api_key, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_find_pets_by_status_PetApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => Vector{Pet}, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_find_pets_by_status(_api::PetApi, status::Vector{String}; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_find_pets_by_status_PetApi, "/pet/findByStatus", ["petstore_auth", ]) - OpenAPI.Clients.set_param(_ctx.query, "status", status; style="form", is_explode=true) # type Vector{String} - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", "application/xml", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Finds Pets by status - -Multiple status values can be provided with comma separated strings - -Params: -- status::Vector{String} (required) - -Return: Vector{Pet}, OpenAPI.Clients.ApiResponse -""" -function find_pets_by_status(_api::PetApi, status::Vector{String}; _mediaType=nothing) - _ctx = _oacinternal_find_pets_by_status(_api, status; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function find_pets_by_status(_api::PetApi, response_stream::Channel, status::Vector{String}; _mediaType=nothing) - _ctx = _oacinternal_find_pets_by_status(_api, status; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_find_pets_by_tags_PetApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => Vector{Pet}, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_find_pets_by_tags(_api::PetApi, tags::Vector{String}; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_find_pets_by_tags_PetApi, "/pet/findByTags", ["petstore_auth", ]) - OpenAPI.Clients.set_param(_ctx.query, "tags", tags; style="form", is_explode=true) # type Vector{String} - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", "application/xml", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Finds Pets by tags - -Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. - -Params: -- tags::Vector{String} (required) - -Return: Vector{Pet}, OpenAPI.Clients.ApiResponse -""" -function find_pets_by_tags(_api::PetApi, tags::Vector{String}; _mediaType=nothing) - _ctx = _oacinternal_find_pets_by_tags(_api, tags; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function find_pets_by_tags(_api::PetApi, response_stream::Channel, tags::Vector{String}; _mediaType=nothing) - _ctx = _oacinternal_find_pets_by_tags(_api, tags; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_get_pet_by_id_PetApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => Pet, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_get_pet_by_id(_api::PetApi, pet_id::Int64; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_get_pet_by_id_PetApi, "/pet/{petId}", ["api_key", ]) - OpenAPI.Clients.set_param(_ctx.path, "petId", pet_id) # type Int64 - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", "application/xml", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Find pet by ID - -Returns a single pet - -Params: -- pet_id::Int64 (required) - -Return: Pet, OpenAPI.Clients.ApiResponse -""" -function get_pet_by_id(_api::PetApi, pet_id::Int64; _mediaType=nothing) - _ctx = _oacinternal_get_pet_by_id(_api, pet_id; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function get_pet_by_id(_api::PetApi, response_stream::Channel, pet_id::Int64; _mediaType=nothing) - _ctx = _oacinternal_get_pet_by_id(_api, pet_id; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_update_pet_PetApi = Dict{Regex,Type}( - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("405", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_update_pet(_api::PetApi, body::Pet; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "PUT", _returntypes_update_pet_PetApi, "/pet", ["petstore_auth", ], body) - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", "application/xml", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Update an existing pet - -Params: -- body::Pet (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function update_pet(_api::PetApi, body::Pet; _mediaType=nothing) - _ctx = _oacinternal_update_pet(_api, body; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function update_pet(_api::PetApi, response_stream::Channel, body::Pet; _mediaType=nothing) - _ctx = _oacinternal_update_pet(_api, body; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_update_pet_with_form_PetApi = Dict{Regex,Type}( - Regex("^" * replace("405", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_update_pet_with_form(_api::PetApi, pet_id::Int64; name=nothing, status=nothing, _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_update_pet_with_form_PetApi, "/pet/{petId}", ["petstore_auth", ]) - OpenAPI.Clients.set_param(_ctx.path, "petId", pet_id) # type Int64 - OpenAPI.Clients.set_param(_ctx.form, "name", name) # type String - OpenAPI.Clients.set_param(_ctx.form, "status", status) # type String - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/x-www-form-urlencoded", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Updates a pet in the store with form data - -Params: -- pet_id::Int64 (required) -- name::String -- status::String - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function update_pet_with_form(_api::PetApi, pet_id::Int64; name=nothing, status=nothing, _mediaType=nothing) - _ctx = _oacinternal_update_pet_with_form(_api, pet_id; name=name, status=status, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function update_pet_with_form(_api::PetApi, response_stream::Channel, pet_id::Int64; name=nothing, status=nothing, _mediaType=nothing) - _ctx = _oacinternal_update_pet_with_form(_api, pet_id; name=name, status=status, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_upload_file_PetApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => ApiResponse, -) - -function _oacinternal_upload_file(_api::PetApi, pet_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_upload_file_PetApi, "/pet/{petId}/uploadImage", ["petstore_auth", ]) - OpenAPI.Clients.set_param(_ctx.path, "petId", pet_id) # type Int64 - OpenAPI.Clients.set_param(_ctx.form, "additionalMetadata", additional_metadata) # type String - OpenAPI.Clients.set_param(_ctx.file, "file", file) # type String - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["multipart/form-data", ] : [_mediaType]) - return _ctx -end - -@doc raw"""uploads an image - -Params: -- pet_id::Int64 (required) -- additional_metadata::String -- file::String - -Return: ApiResponse, OpenAPI.Clients.ApiResponse -""" -function upload_file(_api::PetApi, pet_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) - _ctx = _oacinternal_upload_file(_api, pet_id; additional_metadata=additional_metadata, file=file, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function upload_file(_api::PetApi, response_stream::Channel, pet_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) - _ctx = _oacinternal_upload_file(_api, pet_id; additional_metadata=additional_metadata, file=file, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -export add_pet -export delete_pet -export find_pets_by_status -export find_pets_by_tags -export get_pet_by_id -export update_pet -export update_pet_with_form -export upload_file diff --git a/test/client/petstore_v2/petstore/src/apis/api_StoreApi.jl b/test/client/petstore_v2/petstore/src/apis/api_StoreApi.jl deleted file mode 100644 index 6faff14..0000000 --- a/test/client/petstore_v2/petstore/src/apis/api_StoreApi.jl +++ /dev/null @@ -1,145 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -struct StoreApi <: OpenAPI.APIClientImpl - client::OpenAPI.Clients.Client -end - -""" -The default API base path for APIs in `StoreApi`. -This can be used to construct the `OpenAPI.Clients.Client` instance. -""" -basepath(::Type{ StoreApi }) = "https://petstore.swagger.io/v2" - -const _returntypes_delete_order_StoreApi = Dict{Regex,Type}( - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_delete_order(_api::StoreApi, order_id::Int64; _mediaType=nothing) - OpenAPI.validate_param("order_id", "delete_order", :minimum, order_id, 1, false) - - _ctx = OpenAPI.Clients.Ctx(_api.client, "DELETE", _returntypes_delete_order_StoreApi, "/store/order/{orderId}", []) - OpenAPI.Clients.set_param(_ctx.path, "orderId", order_id) # type Int64 - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Delete purchase order by ID - -For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors - -Params: -- order_id::Int64 (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function delete_order(_api::StoreApi, order_id::Int64; _mediaType=nothing) - _ctx = _oacinternal_delete_order(_api, order_id; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function delete_order(_api::StoreApi, response_stream::Channel, order_id::Int64; _mediaType=nothing) - _ctx = _oacinternal_delete_order(_api, order_id; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_get_inventory_StoreApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => Dict{String, Int64}, -) - -function _oacinternal_get_inventory(_api::StoreApi; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_get_inventory_StoreApi, "/store/inventory", ["api_key", ]) - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Returns pet inventories by status - -Returns a map of status codes to quantities - -Params: - -Return: Dict{String, Int64}, OpenAPI.Clients.ApiResponse -""" -function get_inventory(_api::StoreApi; _mediaType=nothing) - _ctx = _oacinternal_get_inventory(_api; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function get_inventory(_api::StoreApi, response_stream::Channel; _mediaType=nothing) - _ctx = _oacinternal_get_inventory(_api; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_get_order_by_id_StoreApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => Order, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_get_order_by_id(_api::StoreApi, order_id::Int64; _mediaType=nothing) - OpenAPI.validate_param("order_id", "get_order_by_id", :maximum, order_id, 10, false) - OpenAPI.validate_param("order_id", "get_order_by_id", :minimum, order_id, 1, false) - - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_get_order_by_id_StoreApi, "/store/order/{orderId}", []) - OpenAPI.Clients.set_param(_ctx.path, "orderId", order_id) # type Int64 - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", "application/xml", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Find purchase order by ID - -For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions - -Params: -- order_id::Int64 (required) - -Return: Order, OpenAPI.Clients.ApiResponse -""" -function get_order_by_id(_api::StoreApi, order_id::Int64; _mediaType=nothing) - _ctx = _oacinternal_get_order_by_id(_api, order_id; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function get_order_by_id(_api::StoreApi, response_stream::Channel, order_id::Int64; _mediaType=nothing) - _ctx = _oacinternal_get_order_by_id(_api, order_id; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_place_order_StoreApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => Order, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_place_order(_api::StoreApi, body::Order; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_place_order_StoreApi, "/store/order", [], body) - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", "application/xml", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Place an order for a pet - -Params: -- body::Order (required) - -Return: Order, OpenAPI.Clients.ApiResponse -""" -function place_order(_api::StoreApi, body::Order; _mediaType=nothing) - _ctx = _oacinternal_place_order(_api, body; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function place_order(_api::StoreApi, response_stream::Channel, body::Order; _mediaType=nothing) - _ctx = _oacinternal_place_order(_api, body; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -export delete_order -export get_inventory -export get_order_by_id -export place_order diff --git a/test/client/petstore_v2/petstore/src/apis/api_UserApi.jl b/test/client/petstore_v2/petstore/src/apis/api_UserApi.jl deleted file mode 100644 index 3515b3d..0000000 --- a/test/client/petstore_v2/petstore/src/apis/api_UserApi.jl +++ /dev/null @@ -1,262 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -struct UserApi <: OpenAPI.APIClientImpl - client::OpenAPI.Clients.Client -end - -""" -The default API base path for APIs in `UserApi`. -This can be used to construct the `OpenAPI.Clients.Client` instance. -""" -basepath(::Type{ UserApi }) = "https://petstore.swagger.io/v2" - -const _returntypes_create_user_UserApi = Dict{Regex,Type}( - Regex("^" * replace("0", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_create_user(_api::UserApi, body::User; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_create_user_UserApi, "/user", [], body) - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Create user - -This can only be done by the logged in user. - -Params: -- body::User (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function create_user(_api::UserApi, body::User; _mediaType=nothing) - _ctx = _oacinternal_create_user(_api, body; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function create_user(_api::UserApi, response_stream::Channel, body::User; _mediaType=nothing) - _ctx = _oacinternal_create_user(_api, body; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_create_users_with_array_input_UserApi = Dict{Regex,Type}( - Regex("^" * replace("0", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_create_users_with_array_input(_api::UserApi, body::Vector{User}; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_create_users_with_array_input_UserApi, "/user/createWithArray", [], body) - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Creates list of users with given input array - -Params: -- body::Vector{User} (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function create_users_with_array_input(_api::UserApi, body::Vector{User}; _mediaType=nothing) - _ctx = _oacinternal_create_users_with_array_input(_api, body; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function create_users_with_array_input(_api::UserApi, response_stream::Channel, body::Vector{User}; _mediaType=nothing) - _ctx = _oacinternal_create_users_with_array_input(_api, body; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_create_users_with_list_input_UserApi = Dict{Regex,Type}( - Regex("^" * replace("0", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_create_users_with_list_input(_api::UserApi, body::Vector{User}; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_create_users_with_list_input_UserApi, "/user/createWithList", [], body) - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Creates list of users with given input array - -Params: -- body::Vector{User} (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function create_users_with_list_input(_api::UserApi, body::Vector{User}; _mediaType=nothing) - _ctx = _oacinternal_create_users_with_list_input(_api, body; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function create_users_with_list_input(_api::UserApi, response_stream::Channel, body::Vector{User}; _mediaType=nothing) - _ctx = _oacinternal_create_users_with_list_input(_api, body; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_delete_user_UserApi = Dict{Regex,Type}( - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_delete_user(_api::UserApi, username::String; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "DELETE", _returntypes_delete_user_UserApi, "/user/{username}", []) - OpenAPI.Clients.set_param(_ctx.path, "username", username) # type String - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Delete user - -This can only be done by the logged in user. - -Params: -- username::String (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function delete_user(_api::UserApi, username::String; _mediaType=nothing) - _ctx = _oacinternal_delete_user(_api, username; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function delete_user(_api::UserApi, response_stream::Channel, username::String; _mediaType=nothing) - _ctx = _oacinternal_delete_user(_api, username; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_get_user_by_name_UserApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => User, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_get_user_by_name(_api::UserApi, username::String; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_get_user_by_name_UserApi, "/user/{username}", []) - OpenAPI.Clients.set_param(_ctx.path, "username", username) # type String - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", "application/xml", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Get user by user name - -Params: -- username::String (required) - -Return: User, OpenAPI.Clients.ApiResponse -""" -function get_user_by_name(_api::UserApi, username::String; _mediaType=nothing) - _ctx = _oacinternal_get_user_by_name(_api, username; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function get_user_by_name(_api::UserApi, response_stream::Channel, username::String; _mediaType=nothing) - _ctx = _oacinternal_get_user_by_name(_api, username; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_login_user_UserApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => String, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_login_user(_api::UserApi, username::String, password::String; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_login_user_UserApi, "/user/login", []) - OpenAPI.Clients.set_param(_ctx.query, "username", username; style="", is_explode=false) # type String - OpenAPI.Clients.set_param(_ctx.query, "password", password; style="", is_explode=false) # type String - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", "application/xml", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Logs user into the system - -Params: -- username::String (required) -- password::String (required) - -Return: String, OpenAPI.Clients.ApiResponse -""" -function login_user(_api::UserApi, username::String, password::String; _mediaType=nothing) - _ctx = _oacinternal_login_user(_api, username, password; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function login_user(_api::UserApi, response_stream::Channel, username::String, password::String; _mediaType=nothing) - _ctx = _oacinternal_login_user(_api, username, password; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_logout_user_UserApi = Dict{Regex,Type}( - Regex("^" * replace("0", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_logout_user(_api::UserApi; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_logout_user_UserApi, "/user/logout", []) - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Logs out current logged in user session - -Params: - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function logout_user(_api::UserApi; _mediaType=nothing) - _ctx = _oacinternal_logout_user(_api; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function logout_user(_api::UserApi, response_stream::Channel; _mediaType=nothing) - _ctx = _oacinternal_logout_user(_api; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_update_user_UserApi = Dict{Regex,Type}( - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_update_user(_api::UserApi, username::String, body::User; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "PUT", _returntypes_update_user_UserApi, "/user/{username}", [], body) - OpenAPI.Clients.set_param(_ctx.path, "username", username) # type String - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Updated user - -This can only be done by the logged in user. - -Params: -- username::String (required) -- body::User (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function update_user(_api::UserApi, username::String, body::User; _mediaType=nothing) - _ctx = _oacinternal_update_user(_api, username, body; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function update_user(_api::UserApi, response_stream::Channel, username::String, body::User; _mediaType=nothing) - _ctx = _oacinternal_update_user(_api, username, body; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -export create_user -export create_users_with_array_input -export create_users_with_list_input -export delete_user -export get_user_by_name -export login_user -export logout_user -export update_user diff --git a/test/client/petstore_v2/petstore/src/modelincludes.jl b/test/client/petstore_v2/petstore/src/modelincludes.jl deleted file mode 100644 index b3a3db8..0000000 --- a/test/client/petstore_v2/petstore/src/modelincludes.jl +++ /dev/null @@ -1,9 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -include("models/model_ApiResponse.jl") -include("models/model_Category.jl") -include("models/model_Order.jl") -include("models/model_Pet.jl") -include("models/model_Tag.jl") -include("models/model_User.jl") diff --git a/test/client/petstore_v2/petstore/src/models/model_ApiResponse.jl b/test/client/petstore_v2/petstore/src/models/model_ApiResponse.jl deleted file mode 100644 index a196ba0..0000000 --- a/test/client/petstore_v2/petstore/src/models/model_ApiResponse.jl +++ /dev/null @@ -1,49 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""ApiResponse - - ApiResponse(; - code=nothing, - type=nothing, - message=nothing, - ) - - - code::Int64 - - type::String - - message::String -""" -Base.@kwdef mutable struct ApiResponse <: OpenAPI.APIModel - code::Union{Nothing, Int64} = nothing - type::Union{Nothing, String} = nothing - message::Union{Nothing, String} = nothing - - function ApiResponse(code, type, message, ) - o = new(code, type, message, ) - OpenAPI.validate_properties(o) - return o - end -end # type ApiResponse - -const _property_types_ApiResponse = Dict{Symbol,String}(Symbol("code")=>"Int64", Symbol("type")=>"String", Symbol("message")=>"String", ) -OpenAPI.property_type(::Type{ ApiResponse }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_ApiResponse[name]))} - -function OpenAPI.check_required(o::ApiResponse) - true -end - -function OpenAPI.validate_properties(o::ApiResponse) - OpenAPI.validate_property(ApiResponse, Symbol("code"), o.code) - OpenAPI.validate_property(ApiResponse, Symbol("type"), o.type) - OpenAPI.validate_property(ApiResponse, Symbol("message"), o.message) -end - -function OpenAPI.validate_property(::Type{ ApiResponse }, name::Symbol, val) - - if name === Symbol("code") - OpenAPI.validate_param(name, "ApiResponse", :format, val, "int32") - end - - -end diff --git a/test/client/petstore_v2/petstore/src/models/model_Category.jl b/test/client/petstore_v2/petstore/src/models/model_Category.jl deleted file mode 100644 index 159890d..0000000 --- a/test/client/petstore_v2/petstore/src/models/model_Category.jl +++ /dev/null @@ -1,44 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Category - - Category(; - id=nothing, - name=nothing, - ) - - - id::Int64 - - name::String -""" -Base.@kwdef mutable struct Category <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - name::Union{Nothing, String} = nothing - - function Category(id, name, ) - o = new(id, name, ) - OpenAPI.validate_properties(o) - return o - end -end # type Category - -const _property_types_Category = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("name")=>"String", ) -OpenAPI.property_type(::Type{ Category }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Category[name]))} - -function OpenAPI.check_required(o::Category) - true -end - -function OpenAPI.validate_properties(o::Category) - OpenAPI.validate_property(Category, Symbol("id"), o.id) - OpenAPI.validate_property(Category, Symbol("name"), o.name) -end - -function OpenAPI.validate_property(::Type{ Category }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "Category", :format, val, "int64") - end - -end diff --git a/test/client/petstore_v2/petstore/src/models/model_Order.jl b/test/client/petstore_v2/petstore/src/models/model_Order.jl deleted file mode 100644 index a11ca51..0000000 --- a/test/client/petstore_v2/petstore/src/models/model_Order.jl +++ /dev/null @@ -1,77 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Order - - Order(; - id=nothing, - petId=nothing, - quantity=nothing, - shipDate=nothing, - status=nothing, - complete=nothing, - ) - - - id::Int64 - - petId::Int64 - - quantity::Int64 - - shipDate::ZonedDateTime - - status::String : Order Status - - complete::Bool -""" -Base.@kwdef mutable struct Order <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - petId::Union{Nothing, Int64} = nothing - quantity::Union{Nothing, Int64} = nothing - shipDate::Union{Nothing, ZonedDateTime} = nothing - status::Union{Nothing, String} = nothing - complete::Union{Nothing, Bool} = nothing - - function Order(id, petId, quantity, shipDate, status, complete, ) - o = new(id, petId, quantity, shipDate, status, complete, ) - OpenAPI.validate_properties(o) - return o - end -end # type Order - -const _property_types_Order = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("petId")=>"Int64", Symbol("quantity")=>"Int64", Symbol("shipDate")=>"ZonedDateTime", Symbol("status")=>"String", Symbol("complete")=>"Bool", ) -OpenAPI.property_type(::Type{ Order }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Order[name]))} - -function OpenAPI.check_required(o::Order) - true -end - -function OpenAPI.validate_properties(o::Order) - OpenAPI.validate_property(Order, Symbol("id"), o.id) - OpenAPI.validate_property(Order, Symbol("petId"), o.petId) - OpenAPI.validate_property(Order, Symbol("quantity"), o.quantity) - OpenAPI.validate_property(Order, Symbol("shipDate"), o.shipDate) - OpenAPI.validate_property(Order, Symbol("status"), o.status) - OpenAPI.validate_property(Order, Symbol("complete"), o.complete) -end - -function OpenAPI.validate_property(::Type{ Order }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "Order", :format, val, "int64") - end - - if name === Symbol("petId") - OpenAPI.validate_param(name, "Order", :format, val, "int64") - end - - if name === Symbol("quantity") - OpenAPI.validate_param(name, "Order", :format, val, "int32") - end - - if name === Symbol("shipDate") - OpenAPI.validate_param(name, "Order", :format, val, "date-time") - end - - if name === Symbol("status") - OpenAPI.validate_param(name, "Order", :enum, val, ["placed", "approved", "delivered"]) - end - - -end diff --git a/test/client/petstore_v2/petstore/src/models/model_Pet.jl b/test/client/petstore_v2/petstore/src/models/model_Pet.jl deleted file mode 100644 index 322d93a..0000000 --- a/test/client/petstore_v2/petstore/src/models/model_Pet.jl +++ /dev/null @@ -1,70 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Pet - - Pet(; - id=nothing, - category=nothing, - name=nothing, - photoUrls=nothing, - tags=nothing, - status=nothing, - ) - - - id::Int64 - - category::Category - - name::String - - photoUrls::Vector{String} - - tags::Vector{Tag} - - status::String : pet status in the store -""" -Base.@kwdef mutable struct Pet <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - category = nothing # spec type: Union{ Nothing, Category } - name::Union{Nothing, String} = nothing - photoUrls::Union{Nothing, Vector{String}} = nothing - tags::Union{Nothing, Vector} = nothing # spec type: Union{ Nothing, Vector{Tag} } - status::Union{Nothing, String} = nothing - - function Pet(id, category, name, photoUrls, tags, status, ) - o = new(id, category, name, photoUrls, tags, status, ) - OpenAPI.validate_properties(o) - return o - end -end # type Pet - -const _property_types_Pet = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("category")=>"Category", Symbol("name")=>"String", Symbol("photoUrls")=>"Vector{String}", Symbol("tags")=>"Vector{Tag}", Symbol("status")=>"String", ) -OpenAPI.property_type(::Type{ Pet }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Pet[name]))} - -function OpenAPI.check_required(o::Pet) - o.name === nothing && (return false) - o.photoUrls === nothing && (return false) - true -end - -function OpenAPI.validate_properties(o::Pet) - OpenAPI.validate_property(Pet, Symbol("id"), o.id) - OpenAPI.validate_property(Pet, Symbol("category"), o.category) - OpenAPI.validate_property(Pet, Symbol("name"), o.name) - OpenAPI.validate_property(Pet, Symbol("photoUrls"), o.photoUrls) - OpenAPI.validate_property(Pet, Symbol("tags"), o.tags) - OpenAPI.validate_property(Pet, Symbol("status"), o.status) -end - -function OpenAPI.validate_property(::Type{ Pet }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "Pet", :format, val, "int64") - end - - - - - - if name === Symbol("status") - OpenAPI.validate_param(name, "Pet", :enum, val, ["available", "pending", "sold"]) - end - -end diff --git a/test/client/petstore_v2/petstore/src/models/model_Tag.jl b/test/client/petstore_v2/petstore/src/models/model_Tag.jl deleted file mode 100644 index 83051f1..0000000 --- a/test/client/petstore_v2/petstore/src/models/model_Tag.jl +++ /dev/null @@ -1,44 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Tag - - Tag(; - id=nothing, - name=nothing, - ) - - - id::Int64 - - name::String -""" -Base.@kwdef mutable struct Tag <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - name::Union{Nothing, String} = nothing - - function Tag(id, name, ) - o = new(id, name, ) - OpenAPI.validate_properties(o) - return o - end -end # type Tag - -const _property_types_Tag = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("name")=>"String", ) -OpenAPI.property_type(::Type{ Tag }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Tag[name]))} - -function OpenAPI.check_required(o::Tag) - true -end - -function OpenAPI.validate_properties(o::Tag) - OpenAPI.validate_property(Tag, Symbol("id"), o.id) - OpenAPI.validate_property(Tag, Symbol("name"), o.name) -end - -function OpenAPI.validate_property(::Type{ Tag }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "Tag", :format, val, "int64") - end - -end diff --git a/test/client/petstore_v2/petstore/src/models/model_User.jl b/test/client/petstore_v2/petstore/src/models/model_User.jl deleted file mode 100644 index 39b2359..0000000 --- a/test/client/petstore_v2/petstore/src/models/model_User.jl +++ /dev/null @@ -1,77 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""User - - User(; - id=nothing, - username=nothing, - firstName=nothing, - lastName=nothing, - email=nothing, - password=nothing, - phone=nothing, - userStatus=nothing, - ) - - - id::Int64 - - username::String - - firstName::String - - lastName::String - - email::String - - password::String - - phone::String - - userStatus::Int64 : User Status -""" -Base.@kwdef mutable struct User <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - username::Union{Nothing, String} = nothing - firstName::Union{Nothing, String} = nothing - lastName::Union{Nothing, String} = nothing - email::Union{Nothing, String} = nothing - password::Union{Nothing, String} = nothing - phone::Union{Nothing, String} = nothing - userStatus::Union{Nothing, Int64} = nothing - - function User(id, username, firstName, lastName, email, password, phone, userStatus, ) - o = new(id, username, firstName, lastName, email, password, phone, userStatus, ) - OpenAPI.validate_properties(o) - return o - end -end # type User - -const _property_types_User = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("username")=>"String", Symbol("firstName")=>"String", Symbol("lastName")=>"String", Symbol("email")=>"String", Symbol("password")=>"String", Symbol("phone")=>"String", Symbol("userStatus")=>"Int64", ) -OpenAPI.property_type(::Type{ User }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_User[name]))} - -function OpenAPI.check_required(o::User) - true -end - -function OpenAPI.validate_properties(o::User) - OpenAPI.validate_property(User, Symbol("id"), o.id) - OpenAPI.validate_property(User, Symbol("username"), o.username) - OpenAPI.validate_property(User, Symbol("firstName"), o.firstName) - OpenAPI.validate_property(User, Symbol("lastName"), o.lastName) - OpenAPI.validate_property(User, Symbol("email"), o.email) - OpenAPI.validate_property(User, Symbol("password"), o.password) - OpenAPI.validate_property(User, Symbol("phone"), o.phone) - OpenAPI.validate_property(User, Symbol("userStatus"), o.userStatus) -end - -function OpenAPI.validate_property(::Type{ User }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "User", :format, val, "int64") - end - - - - - - - - if name === Symbol("userStatus") - OpenAPI.validate_param(name, "User", :format, val, "int32") - end -end diff --git a/test/client/petstore_v2/petstore_test_petapi.jl b/test/client/petstore_v2/petstore_test_petapi.jl deleted file mode 100644 index a1f608c..0000000 --- a/test/client/petstore_v2/petstore_test_petapi.jl +++ /dev/null @@ -1,67 +0,0 @@ -module TestPetApi - -using ..PetStoreClient -using Test -using OpenAPI -using OpenAPI.Clients -import OpenAPI.Clients: Client - -function test(uri, httplib::Symbol) - @info("PetApi ($httplib backend)") - client = Client(uri; httplib=httplib) - api = PetApi(client) - - tag1 = Tag(;id=10, name="juliacat") - tag2 = Tag(;id=11, name="white") - cat = Category(;id=10, name="cat") - - @test_throws OpenAPI.ValidationException Pet(;id=10, category=cat, name="felix", photoUrls=nothing, tags=[tag1, tag2], status="invalid-status") - - pet = Pet(;id=10, category=cat, name="felix", photoUrls=["http://photo/1","http://photo/2"], tags=[tag1,tag2], status="pending") - - @info("PetApi - add_pet") - api_return, http_resp = add_pet(api, pet) - @test api_return === nothing - @test http_resp.status == 200 - - @info("PetApi - update_pet") - pet.status = "available" - api_return, http_resp = update_pet(api, pet) - @test api_return === nothing - @test http_resp.status == 200 - - # @info("PetApi - updatePetWithForm") - # @test updatePetWithForm(api, 10; in_name="meow") === nothing - - @info("PetApi - get_pet_by_id") - pet10, http_resp = get_pet_by_id(api, Int64(10)) - @test http_resp.status == 200 - @test pet10.id == 10 - - @info("PetApi - find_pets_by_status") - unsold = ["available", "pending"] - pets, http_resp = find_pets_by_status(api, unsold) - @test http_resp.status == 200 - @test isa(pets, Vector{Pet}) - @info("PetApi - find_pets_by_status", npets=length(pets)) - for p in pets - @test p.status in unsold - end - - @info("PetApi - deletePet") - api_return, http_resp = delete_pet(api, Int64(10)) - @test http_resp.status == 200 - @test api_return === nothing - - # does not work yet. issue: https://github.com/JuliaWeb/Requests.jl/issues/139 - #@info("PetApi - upload_file") - #img = joinpath(dirname(@__FILE__), "cat.png") - #resp, http_resp = upload_file(api, 10; additionalMetadata="juliacat pic", file=img) - #@test isa(resp, ApiResponse) - #@test resp.code == 200 - #@info("PetApi - upload_file", typ=get_field(resp, "type"), message=get_field(resp, "message")) - - nothing -end - -end # module TestPetApi diff --git a/test/client/petstore_v2/petstore_test_storeapi.jl b/test/client/petstore_v2/petstore_test_storeapi.jl deleted file mode 100644 index 69ed184..0000000 --- a/test/client/petstore_v2/petstore_test_storeapi.jl +++ /dev/null @@ -1,73 +0,0 @@ -module TestStoreApi - -using ..PetStoreClient -using Test -using Dates -using TimeZones -using OpenAPI -using OpenAPI.Clients -import OpenAPI.Clients: Client - -function test(uri, httplib::Symbol) - @info("StoreApi ($httplib backend)") - client = Client(uri; httplib=httplib) - api = StoreApi(client) - - @info("StoreApi - get_inventory") - inventory, http_resp = get_inventory(api) - @test http_resp.status == 200 - @test isa(inventory, Dict{String,Int64}) - @test !isempty(inventory) - - @info("StoreApi - place_order") - @test_throws OpenAPI.ValidationException Order(; id=5, petId=10, quantity=2, shipDate=ZonedDateTime(DateTime(2017, 03, 12), localzone()), status="invalid_status", complete=false) - order = Order(; id=5, petId=10, quantity=2, shipDate=ZonedDateTime(DateTime(2017, 03, 12), localzone()), status="placed", complete=false) - neworder, http_resp = place_order(api, order) - @test http_resp.status == 200 - @test neworder.id == 5 - - @info("StoreApi - get_order_by_id") - @test_throws OpenAPI.ValidationException get_order_by_id(api, Int64(0)) - order, http_resp = get_order_by_id(api, Int64(5)) - @test http_resp.status == 200 - @test isa(order, Order) - @test order.id == 5 - @test isa(order.shipDate, ZonedDateTime) - - @info("StoreApi - get_order_by_id (async)") - response_channel = Channel{Order}(1) - @test_throws OpenAPI.ValidationException get_order_by_id(api, response_channel, Int64(0)) - @sync begin - @async begin - resp, http_resp = get_order_by_id(api, response_channel, Int64(5)) - @test (200 <= http_resp.status <= 206) - @test resp === response_channel - end - @async begin - order = take!(response_channel) - @test isa(order, Order) - @test order.id == 5 - end - end - - # a closed channel is equivalent of cancellation of the call, - # no error should be thrown, but response can be nothing if call was interrupted immediately - @test !isopen(response_channel) - - # open a new channel to use - response_channel = Channel{Order}(1) - try - resp, http_resp = get_order_by_id(api, response_channel, Int64(5)) - @test (200 <= http_resp.status <= 206) - catch ex - @test isa(ex, OpenAPI.InvocationException) - end - - @info("StoreApi - delete_order") - api_return, http_resp = delete_order(api, Int64(5)) - @test api_return === nothing - - nothing -end - -end # module TestStoreApi diff --git a/test/client/petstore_v2/petstore_test_userapi.jl b/test/client/petstore_v2/petstore_test_userapi.jl deleted file mode 100644 index 4417da8..0000000 --- a/test/client/petstore_v2/petstore_test_userapi.jl +++ /dev/null @@ -1,179 +0,0 @@ -module TestUserApi - -using ..PetStoreClient -using Test -using Random -using JSON -using URIs -using OpenAPI -using OpenAPI.Clients -import OpenAPI.Clients: Client, Ctx, ApiException, DEFAULT_TIMEOUT_SECS, with_timeout, set_timeout, set_user_agent, set_cookie - -const TEST_USER = "jloac" -const TEST_USER1 = "jloac1" -const TEST_USER2 = "jloac2" -const PRESET_TEST_USER = "user1" # this is the username that works for get user requests (as documented in the test docker container API) - -function test_404(uri, httplib::Symbol) - @info("Error handling ($httplib backend)") - client = Client(uri*"_invalid"; httplib=httplib) - api = UserApi(client) - - api_return, http_resp = login_user(api, TEST_USER, "testpassword") - @test http_resp.status == 404 - @test api_return === nothing - - client = Client("http://_invalid/"; httplib=httplib) - api = UserApi(client) - - try - login_user(api, TEST_USER, "testpassword") - @error("ApiException not thrown") - catch ex - @test isa(ex, ApiException) - @test startswith(ex.reason, "Could not resolve host") || startswith(ex.reason, "DNSError") - end -end - -function test_set_methods() - @info("Error handling") - client = Client("http://_invalid/") - - @test client.timeout[] == DEFAULT_TIMEOUT_SECS - - with_timeout(client, DEFAULT_TIMEOUT_SECS + 10) do client - @test client.timeout[] == DEFAULT_TIMEOUT_SECS + 10 - end - @test client.timeout[] == DEFAULT_TIMEOUT_SECS - - api = UserApi(client) - with_timeout(api, DEFAULT_TIMEOUT_SECS + 10) do api - @test api.client.timeout[] == DEFAULT_TIMEOUT_SECS + 10 - end - @test client.timeout[] == DEFAULT_TIMEOUT_SECS - - set_timeout(client, DEFAULT_TIMEOUT_SECS + 10) - @test client.timeout[] == DEFAULT_TIMEOUT_SECS + 10 - - @test isempty(client.headers) - set_user_agent(client, "007") - set_cookie(client, "crumbly") - @test client.headers["User-Agent"] == "007" - @test client.headers["Cookie"] == "crumbly" -end - -function test_login_user_hook(ctx::Ctx) - ctx.header["actual_password"] = "testpassword" - ctx -end - -function test_login_user_hook(resource_path::AbstractString, body::Any, headers::Dict{String,String}) - uri = URIs.parse_uri(resource_path) - qparams = URIs.queryparams(uri) - qparams["password"] = headers["actual_password"] - delete!(headers, "actual_password") - resource_path = string(URIs.URI(uri; query=escapeuri(qparams))) - - (resource_path, body, headers) -end - -function test_userhook(uri, httplib::Symbol) - @info("User hook ($httplib backend)") - client = Client(uri; pre_request_hook=test_login_user_hook, httplib=httplib) - api = UserApi(client) - - login_result, http_resp = login_user(api, TEST_USER, "wrongpassword") - @test http_resp.status == 200 - @test !isempty(login_result) - result = JSON.parse(login_result) - @test startswith(result["message"], "logged in user session") - @test result["code"] == 200 -end - -function test_parallel(uri, httplib::Symbol) - @info("Parallel usage ($httplib backend)") - client = Client(uri; httplib=httplib) - api = UserApi(client) - - for gcidx in 1:100 - @sync begin - for idx in 1:10^3 - @async begin - @debug("[$idx] UserApi Parallel begin") - login_result = login_user(api, TEST_USER, "testpassword") - @test !isempty(login_result) - result = JSON.parse(login_result) - @test startswith(result["message"], "logged in user session") - @test result["code"] == 200 - - @test_throws ApiException get_user_by_name(api, randstring()) - @test_throws ApiException get_user_by_name(api, TEST_USER) - - logout_result = logout_user(api) - @test logout_result === nothing - @debug("[$idx] UserApi Parallel end") - end - end - end - GC.gc() - @info("outer loop $gcidx") - end - nothing -end - -function test(uri, httplib::Symbol) - @info("UserApi ($httplib backend)") - client = Client(uri; httplib=httplib) - api = UserApi(client) - - @info("UserApi - login_user") - login_result, http_resp = login_user(api, TEST_USER, "testpassword") - @test !isempty(login_result) - @test http_resp.status == 200 - - @info("UserApi - create_user") - user1 = User(; id=100, username=TEST_USER1, firstName="test1", lastName="user1", email="jloac1@example.com", password="testpass1", phone="1000000001", userStatus=0) - api_return, http_resp = create_user(api, user1) - @test api_return === nothing - @test http_resp.status == 200 - - @info("UserApi - create_users_with_array_input") - user2 = User(; id=200, username=TEST_USER2, firstName="test2", lastName="user2", email="jloac2@example.com", password="testpass2", phone="1000000002", userStatus=0) - api_return, http_resp = create_users_with_array_input(api, [user1, user2]) - @test api_return === nothing - @test http_resp.status == 200 - - @info("UserApi - create_users_with_array_input") - api_return, http_resp = create_users_with_array_input(api, [user1, user2]) - @test api_return === nothing - @test http_resp.status == 200 - - @info("UserApi - get_user_by_name") - getuser_result, http_resp = get_user_by_name(api, randstring()) - @test getuser_result === nothing - @test http_resp.status == 404 - getuser_result, http_resp = get_user_by_name(api, TEST_USER) - @test getuser_result === nothing - @test http_resp.status == 404 - getuser_result, http_resp = get_user_by_name(api, PRESET_TEST_USER) - @test isa(getuser_result, User) - @test http_resp.status == 200 - - @info("UserApi - update_user") - api_return, http_resp = update_user(api, TEST_USER2, getuser_result) - @test api_return === nothing - @test http_resp.status == 200 - @info("UserApi - delete_user") - api_return, http_resp = delete_user(api, TEST_USER2) - @test api_return === nothing - @test http_resp.status == 200 - - @info("UserApi - logout_user") - logout_result, http_resp = logout_user(api) - @test logout_result === nothing - @test http_resp.status == 200 - - nothing -end - -end # module TestUserApi diff --git a/test/client/petstore_v2/runtests.jl b/test/client/petstore_v2/runtests.jl deleted file mode 100644 index 6d2cea4..0000000 --- a/test/client/petstore_v2/runtests.jl +++ /dev/null @@ -1,44 +0,0 @@ -module PetStoreV2Tests - -include(joinpath(@__DIR__, "petstore", "src", "PetStoreClient.jl")) -using .PetStoreClient -using Test - -include("petstore_test_petapi.jl") -include("petstore_test_userapi.jl") -include("petstore_test_storeapi.jl") - -const server = "http://127.0.0.1:8080/v2" - -function test_misc(httplib::Symbol) - TestUserApi.test_404(server, httplib) - TestUserApi.test_userhook(server, httplib) - TestUserApi.test_set_methods() -end - -function test_stress(httplib::Symbol) - TestUserApi.test_parallel(server, httplib) -end - -function petstore_tests(httplib::Symbol) - TestUserApi.test(server, httplib) - TestStoreApi.test(server, httplib) - TestPetApi.test(server, httplib) -end - -function runtests(httplib::Symbol) - @testset "petstore v2" begin - @testset "miscellaneous" begin - test_misc(httplib) - end - @testset "petstore apis" begin - petstore_tests(httplib) - end - if get(ENV, "STRESS_PETSTORE", "false") == "true" - @testset "stress" begin - test_stress(httplib) - end - end - end -end -end # module PetStoreV2Tests \ No newline at end of file diff --git a/test/client/petstore_v2/start_petstore_server.sh b/test/client/petstore_v2/start_petstore_server.sh deleted file mode 100755 index c2b9040..0000000 --- a/test/client/petstore_v2/start_petstore_server.sh +++ /dev/null @@ -1,4 +0,0 @@ -docker stop swagger-petstore 2> /dev/null -docker rm swagger-petstore 2> /dev/null -docker pull swaggerapi/petstore:latest -docker run --rm -d --name swagger-petstore -e SWAGGER_HOST=http://127.0.0.1 -e SWAGGER_BASE_PATH=/v2 -p 8080:8080 swaggerapi/petstore:latest \ No newline at end of file diff --git a/test/client/petstore_v2/stop_petstore_server.sh b/test/client/petstore_v2/stop_petstore_server.sh deleted file mode 100755 index 741c322..0000000 --- a/test/client/petstore_v2/stop_petstore_server.sh +++ /dev/null @@ -1,4 +0,0 @@ -echo "stopping swagger-petstore server" -docker stop swagger-petstore -docker rm swagger-petstore 2>/dev/null -echo "stopped swagger-petstore server" \ No newline at end of file diff --git a/test/client/petstore_v3/generate.sh b/test/client/petstore_v3/generate.sh deleted file mode 100755 index eff241c..0000000 --- a/test/client/petstore_v3/generate.sh +++ /dev/null @@ -1,7 +0,0 @@ -java -jar openapi-generator-cli.jar generate \ - -i ../../specs/petstore_v3.json \ - -g julia-client \ - -o petstore \ - --additional-properties=packageName=PetStoreClient \ - --additional-properties=exportModels=true \ - --additional-properties=exportOperations=true diff --git a/test/client/petstore_v3/petstore/.openapi-generator-ignore b/test/client/petstore_v3/petstore/.openapi-generator-ignore deleted file mode 100644 index 7484ee5..0000000 --- a/test/client/petstore_v3/petstore/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/test/client/petstore_v3/petstore/.openapi-generator/FILES b/test/client/petstore_v3/petstore/.openapi-generator/FILES deleted file mode 100644 index b9cea9e..0000000 --- a/test/client/petstore_v3/petstore/.openapi-generator/FILES +++ /dev/null @@ -1,21 +0,0 @@ -README.md -docs/ApiResponse.md -docs/Category.md -docs/Order.md -docs/Pet.md -docs/PetApi.md -docs/StoreApi.md -docs/Tag.md -docs/User.md -docs/UserApi.md -src/PetStoreClient.jl -src/apis/api_PetApi.jl -src/apis/api_StoreApi.jl -src/apis/api_UserApi.jl -src/modelincludes.jl -src/models/model_ApiResponse.jl -src/models/model_Category.jl -src/models/model_Order.jl -src/models/model_Pet.jl -src/models/model_Tag.jl -src/models/model_User.jl diff --git a/test/client/petstore_v3/petstore/.openapi-generator/VERSION b/test/client/petstore_v3/petstore/.openapi-generator/VERSION deleted file mode 100644 index 96cfbb1..0000000 --- a/test/client/petstore_v3/petstore/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.13.0-SNAPSHOT diff --git a/test/client/petstore_v3/petstore/README.md b/test/client/petstore_v3/petstore/README.md deleted file mode 100644 index bb7aa74..0000000 --- a/test/client/petstore_v3/petstore/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# Julia API client for PetStoreClient - -This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. For OAuth2 flow, you may use `user` as both username and password when asked to login. - -## Overview -This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. - -- API version: 1.0.0 -- Generator version: 7.13.0-SNAPSHOT -- Build package: org.openapitools.codegen.languages.JuliaClientCodegen - - -## Installation -Place the Julia files generated under the `src` folder in your Julia project. Include PetStoreClient.jl in the project code. -It would include the module named PetStoreClient. - -Documentation is generated as markdown files under the `docs` folder. You can include them in your project documentation. -Documentation is also embedded in Julia which can be used with a Julia specific documentation generator. - -## API Endpoints - -Class | Method ------------- | ------------- -*PetApi* | [**add_pet**](docs/PetApi.md#add_pet)
**POST** /pet
Add a new pet to the store -*PetApi* | [**delete_pet**](docs/PetApi.md#delete_pet)
**DELETE** /pet/{petId}
Deletes a pet -*PetApi* | [**find_pets_by_status**](docs/PetApi.md#find_pets_by_status)
**GET** /pet/findByStatus
Finds Pets by status -*PetApi* | [**find_pets_by_tags**](docs/PetApi.md#find_pets_by_tags)
**GET** /pet/findByTags
Finds Pets by tags -*PetApi* | [**get_pet_by_id**](docs/PetApi.md#get_pet_by_id)
**GET** /pet/{petId}
Find pet by ID -*PetApi* | [**update_pet**](docs/PetApi.md#update_pet)
**PUT** /pet
Update an existing pet -*PetApi* | [**update_pet_with_form**](docs/PetApi.md#update_pet_with_form)
**POST** /pet/{petId}
Updates a pet in the store with form data -*PetApi* | [**upload_file**](docs/PetApi.md#upload_file)
**POST** /pet/{petId}/uploadImage
uploads an image -*StoreApi* | [**delete_order**](docs/StoreApi.md#delete_order)
**DELETE** /store/order/{orderId}
Delete purchase order by ID -*StoreApi* | [**get_inventory**](docs/StoreApi.md#get_inventory)
**GET** /store/inventory
Returns pet inventories by status -*StoreApi* | [**get_order_by_id**](docs/StoreApi.md#get_order_by_id)
**GET** /store/order/{orderId}
Find purchase order by ID -*StoreApi* | [**place_order**](docs/StoreApi.md#place_order)
**POST** /store/order
Place an order for a pet -*UserApi* | [**create_user**](docs/UserApi.md#create_user)
**POST** /user
Create user -*UserApi* | [**create_users_with_array_input**](docs/UserApi.md#create_users_with_array_input)
**POST** /user/createWithArray
Creates list of users with given input array -*UserApi* | [**create_users_with_list_input**](docs/UserApi.md#create_users_with_list_input)
**POST** /user/createWithList
Creates list of users with given input array -*UserApi* | [**delete_user**](docs/UserApi.md#delete_user)
**DELETE** /user/{username}
Delete user -*UserApi* | [**get_user_by_name**](docs/UserApi.md#get_user_by_name)
**GET** /user/{username}
Get user by user name -*UserApi* | [**login_user**](docs/UserApi.md#login_user)
**GET** /user/login
Logs user into the system -*UserApi* | [**logout_user**](docs/UserApi.md#logout_user)
**GET** /user/logout
Logs out current logged in user session -*UserApi* | [**update_user**](docs/UserApi.md#update_user)
**PUT** /user/{username}
Updated user - - -## Models - - - [ApiResponse](docs/ApiResponse.md) - - [Category](docs/Category.md) - - [Order](docs/Order.md) - - [Pet](docs/Pet.md) - - [Tag](docs/Tag.md) - - [User](docs/User.md) - - - -## Authorization - -Authentication schemes defined for the API: - -### petstore_auth -- **Type**: OAuth -- **Flow**: implicit -- **Authorization URL**: /api/oauth/dialog -- **Scopes**: - - **write:pets**: modify pets in your account - - **read:pets**: read your pets - -Example -``` - using OpenAPI - using OpenAPI.Clients - import OpenAPI.Clients: Client, set_header - client = Client(server_uri) - set_header(client, "Authorization", "Bearer $bearer_auth") - api = MyApi(client) - result = callApi(api, args...; api_key) -``` - -### api_key -- **Type**: API key - -Example -``` - using OpenAPI - using OpenAPI.Clients - import OpenAPI.Clients: Client - client = Client(server_uri) - api = MyApi(client) - result = callApi(api, args...; api_key) -``` - -## Author - - - diff --git a/test/client/petstore_v3/petstore/docs/ApiResponse.md b/test/client/petstore_v3/petstore/docs/ApiResponse.md deleted file mode 100644 index 664dd24..0000000 --- a/test/client/petstore_v3/petstore/docs/ApiResponse.md +++ /dev/null @@ -1,14 +0,0 @@ -# ApiResponse - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**code** | **Int64** | | [optional] [default to nothing] -**type** | **String** | | [optional] [default to nothing] -**message** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/petstore_v3/petstore/docs/Category.md b/test/client/petstore_v3/petstore/docs/Category.md deleted file mode 100644 index 4e93290..0000000 --- a/test/client/petstore_v3/petstore/docs/Category.md +++ /dev/null @@ -1,13 +0,0 @@ -# Category - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**name** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/petstore_v3/petstore/docs/Order.md b/test/client/petstore_v3/petstore/docs/Order.md deleted file mode 100644 index b815c05..0000000 --- a/test/client/petstore_v3/petstore/docs/Order.md +++ /dev/null @@ -1,17 +0,0 @@ -# Order - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**petId** | **Int64** | | [optional] [default to nothing] -**quantity** | **Int64** | | [optional] [default to nothing] -**shipDate** | **ZonedDateTime** | | [optional] [default to nothing] -**status** | **String** | Order Status | [optional] [default to nothing] -**complete** | **Bool** | | [optional] [default to false] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/petstore_v3/petstore/docs/Pet.md b/test/client/petstore_v3/petstore/docs/Pet.md deleted file mode 100644 index a8bba4c..0000000 --- a/test/client/petstore_v3/petstore/docs/Pet.md +++ /dev/null @@ -1,17 +0,0 @@ -# Pet - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**category** | [***Category**](Category.md) | | [optional] [default to nothing] -**name** | **String** | | [default to nothing] -**photoUrls** | **Vector{String}** | | [default to nothing] -**tags** | [**Vector{Tag}**](Tag.md) | | [optional] [default to nothing] -**status** | **String** | pet status in the store | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/petstore_v3/petstore/docs/PetApi.md b/test/client/petstore_v3/petstore/docs/PetApi.md deleted file mode 100644 index e9355d1..0000000 --- a/test/client/petstore_v3/petstore/docs/PetApi.md +++ /dev/null @@ -1,266 +0,0 @@ -# PetApi - -All URIs are relative to */v3* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**add_pet**](PetApi.md#add_pet) | **POST** /pet | Add a new pet to the store -[**delete_pet**](PetApi.md#delete_pet) | **DELETE** /pet/{petId} | Deletes a pet -[**find_pets_by_status**](PetApi.md#find_pets_by_status) | **GET** /pet/findByStatus | Finds Pets by status -[**find_pets_by_tags**](PetApi.md#find_pets_by_tags) | **GET** /pet/findByTags | Finds Pets by tags -[**get_pet_by_id**](PetApi.md#get_pet_by_id) | **GET** /pet/{petId} | Find pet by ID -[**update_pet**](PetApi.md#update_pet) | **PUT** /pet | Update an existing pet -[**update_pet_with_form**](PetApi.md#update_pet_with_form) | **POST** /pet/{petId} | Updates a pet in the store with form data -[**upload_file**](PetApi.md#upload_file) | **POST** /pet/{petId}/uploadImage | uploads an image - - -# **add_pet** -> add_pet(_api::PetApi, pet::Pet; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> add_pet(_api::PetApi, response_stream::Channel, pet::Pet; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Add a new pet to the store - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**pet** | [**Pet**](Pet.md) | Pet object that needs to be added to the store | - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: application/json, application/xml - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **delete_pet** -> delete_pet(_api::PetApi, pet_id::Int64; api_key=nothing, _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> delete_pet(_api::PetApi, response_stream::Channel, pet_id::Int64; api_key=nothing, _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Deletes a pet - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**pet_id** | **Int64** | Pet id to delete | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **api_key** | **String** | | [default to nothing] - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **find_pets_by_status** -> find_pets_by_status(_api::PetApi, status::Vector{String}; _mediaType=nothing) -> Vector{Pet}, OpenAPI.Clients.ApiResponse
-> find_pets_by_status(_api::PetApi, response_stream::Channel, status::Vector{String}; _mediaType=nothing) -> Channel{ Vector{Pet} }, OpenAPI.Clients.ApiResponse - -Finds Pets by status - -Multiple status values can be provided with comma separated strings - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**status** | [**Vector{String}**](String.md) | Status values that need to be considered for filter | - -### Return type - -[**Vector{Pet}**](Pet.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **find_pets_by_tags** -> find_pets_by_tags(_api::PetApi, tags::Vector{String}; _mediaType=nothing) -> Vector{Pet}, OpenAPI.Clients.ApiResponse
-> find_pets_by_tags(_api::PetApi, response_stream::Channel, tags::Vector{String}; _mediaType=nothing) -> Channel{ Vector{Pet} }, OpenAPI.Clients.ApiResponse - -Finds Pets by tags - -Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**tags** | [**Vector{String}**](String.md) | Tags to filter by | - -### Return type - -[**Vector{Pet}**](Pet.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **get_pet_by_id** -> get_pet_by_id(_api::PetApi, pet_id::Int64; _mediaType=nothing) -> Pet, OpenAPI.Clients.ApiResponse
-> get_pet_by_id(_api::PetApi, response_stream::Channel, pet_id::Int64; _mediaType=nothing) -> Channel{ Pet }, OpenAPI.Clients.ApiResponse - -Find pet by ID - -Returns a single pet - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**pet_id** | **Int64** | ID of pet to return | - -### Return type - -[**Pet**](Pet.md) - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **update_pet** -> update_pet(_api::PetApi, pet::Pet; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> update_pet(_api::PetApi, response_stream::Channel, pet::Pet; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Update an existing pet - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**pet** | [**Pet**](Pet.md) | Pet object that needs to be added to the store | - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: application/json, application/xml - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **update_pet_with_form** -> update_pet_with_form(_api::PetApi, pet_id::Int64; name=nothing, status=nothing, _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> update_pet_with_form(_api::PetApi, response_stream::Channel, pet_id::Int64; name=nothing, status=nothing, _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Updates a pet in the store with form data - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**pet_id** | **Int64** | ID of pet that needs to be updated | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **name** | **String** | Updated name of the pet | [default to nothing] - **status** | **String** | Updated status of the pet | [default to nothing] - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: application/x-www-form-urlencoded - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **upload_file** -> upload_file(_api::PetApi, pet_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) -> ApiResponse, OpenAPI.Clients.ApiResponse
-> upload_file(_api::PetApi, response_stream::Channel, pet_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) -> Channel{ ApiResponse }, OpenAPI.Clients.ApiResponse - -uploads an image - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**pet_id** | **Int64** | ID of pet to update | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **additional_metadata** | **String** | Additional data to pass to server | [default to nothing] - **file** | **String** | file to upload | - -### Return type - -[**ApiResponse**](ApiResponse.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: multipart/form-data - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - diff --git a/test/client/petstore_v3/petstore/docs/StoreApi.md b/test/client/petstore_v3/petstore/docs/StoreApi.md deleted file mode 100644 index 6a6f6b9..0000000 --- a/test/client/petstore_v3/petstore/docs/StoreApi.md +++ /dev/null @@ -1,126 +0,0 @@ -# StoreApi - -All URIs are relative to */v3* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**delete_order**](StoreApi.md#delete_order) | **DELETE** /store/order/{orderId} | Delete purchase order by ID -[**get_inventory**](StoreApi.md#get_inventory) | **GET** /store/inventory | Returns pet inventories by status -[**get_order_by_id**](StoreApi.md#get_order_by_id) | **GET** /store/order/{orderId} | Find purchase order by ID -[**place_order**](StoreApi.md#place_order) | **POST** /store/order | Place an order for a pet - - -# **delete_order** -> delete_order(_api::StoreApi, order_id::String; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> delete_order(_api::StoreApi, response_stream::Channel, order_id::String; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Delete purchase order by ID - -For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **StoreApi** | API context | -**order_id** | **String** | ID of the order that needs to be deleted | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **get_inventory** -> get_inventory(_api::StoreApi; _mediaType=nothing) -> Dict{String, Int64}, OpenAPI.Clients.ApiResponse
-> get_inventory(_api::StoreApi, response_stream::Channel; _mediaType=nothing) -> Channel{ Dict{String, Int64} }, OpenAPI.Clients.ApiResponse - -Returns pet inventories by status - -Returns a map of status codes to quantities - -### Required Parameters -This endpoint does not need any parameter. - -### Return type - -**Dict{String, Int64}** - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **get_order_by_id** -> get_order_by_id(_api::StoreApi, order_id::Int64; _mediaType=nothing) -> Order, OpenAPI.Clients.ApiResponse
-> get_order_by_id(_api::StoreApi, response_stream::Channel, order_id::Int64; _mediaType=nothing) -> Channel{ Order }, OpenAPI.Clients.ApiResponse - -Find purchase order by ID - -For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **StoreApi** | API context | -**order_id** | **Int64** | ID of pet that needs to be fetched | - -### Return type - -[**Order**](Order.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **place_order** -> place_order(_api::StoreApi, order::Order; _mediaType=nothing) -> Order, OpenAPI.Clients.ApiResponse
-> place_order(_api::StoreApi, response_stream::Channel, order::Order; _mediaType=nothing) -> Channel{ Order }, OpenAPI.Clients.ApiResponse - -Place an order for a pet - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **StoreApi** | API context | -**order** | [**Order**](Order.md) | order placed for purchasing the pet | - -### Return type - -[**Order**](Order.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - diff --git a/test/client/petstore_v3/petstore/docs/Tag.md b/test/client/petstore_v3/petstore/docs/Tag.md deleted file mode 100644 index c904872..0000000 --- a/test/client/petstore_v3/petstore/docs/Tag.md +++ /dev/null @@ -1,13 +0,0 @@ -# Tag - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**name** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/petstore_v3/petstore/docs/User.md b/test/client/petstore_v3/petstore/docs/User.md deleted file mode 100644 index 5318b5a..0000000 --- a/test/client/petstore_v3/petstore/docs/User.md +++ /dev/null @@ -1,19 +0,0 @@ -# User - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**username** | **String** | | [optional] [default to nothing] -**firstName** | **String** | | [optional] [default to nothing] -**lastName** | **String** | | [optional] [default to nothing] -**email** | **String** | | [optional] [default to nothing] -**password** | **String** | | [optional] [default to nothing] -**phone** | **String** | | [optional] [default to nothing] -**userStatus** | **Int64** | User Status | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/petstore_v3/petstore/docs/UserApi.md b/test/client/petstore_v3/petstore/docs/UserApi.md deleted file mode 100644 index fdb5b07..0000000 --- a/test/client/petstore_v3/petstore/docs/UserApi.md +++ /dev/null @@ -1,244 +0,0 @@ -# UserApi - -All URIs are relative to */v3* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**create_user**](UserApi.md#create_user) | **POST** /user | Create user -[**create_users_with_array_input**](UserApi.md#create_users_with_array_input) | **POST** /user/createWithArray | Creates list of users with given input array -[**create_users_with_list_input**](UserApi.md#create_users_with_list_input) | **POST** /user/createWithList | Creates list of users with given input array -[**delete_user**](UserApi.md#delete_user) | **DELETE** /user/{username} | Delete user -[**get_user_by_name**](UserApi.md#get_user_by_name) | **GET** /user/{username} | Get user by user name -[**login_user**](UserApi.md#login_user) | **GET** /user/login | Logs user into the system -[**logout_user**](UserApi.md#logout_user) | **GET** /user/logout | Logs out current logged in user session -[**update_user**](UserApi.md#update_user) | **PUT** /user/{username} | Updated user - - -# **create_user** -> create_user(_api::UserApi, user::User; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> create_user(_api::UserApi, response_stream::Channel, user::User; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Create user - -This can only be done by the logged in user. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**user** | [**User**](User.md) | Created user object | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **create_users_with_array_input** -> create_users_with_array_input(_api::UserApi, user::Vector{User}; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> create_users_with_array_input(_api::UserApi, response_stream::Channel, user::Vector{User}; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Creates list of users with given input array - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**user** | [**Vector{User}**](User.md) | List of user object | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **create_users_with_list_input** -> create_users_with_list_input(_api::UserApi, user::Vector{User}; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> create_users_with_list_input(_api::UserApi, response_stream::Channel, user::Vector{User}; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Creates list of users with given input array - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**user** | [**Vector{User}**](User.md) | List of user object | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **delete_user** -> delete_user(_api::UserApi, username::String; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> delete_user(_api::UserApi, response_stream::Channel, username::String; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Delete user - -This can only be done by the logged in user. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**username** | **String** | The name that needs to be deleted | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **get_user_by_name** -> get_user_by_name(_api::UserApi, username::String; _mediaType=nothing) -> User, OpenAPI.Clients.ApiResponse
-> get_user_by_name(_api::UserApi, response_stream::Channel, username::String; _mediaType=nothing) -> Channel{ User }, OpenAPI.Clients.ApiResponse - -Get user by user name - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**username** | **String** | The name that needs to be fetched. Use user1 for testing. | - -### Return type - -[**User**](User.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **login_user** -> login_user(_api::UserApi, username::String, password::String; _mediaType=nothing) -> String, OpenAPI.Clients.ApiResponse
-> login_user(_api::UserApi, response_stream::Channel, username::String, password::String; _mediaType=nothing) -> Channel{ String }, OpenAPI.Clients.ApiResponse - -Logs user into the system - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**username** | **String** | The user name for login | -**password** | **String** | The password for login in clear text | - -### Return type - -**String** - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **logout_user** -> logout_user(_api::UserApi; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> logout_user(_api::UserApi, response_stream::Channel; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Logs out current logged in user session - -### Required Parameters -This endpoint does not need any parameter. - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **update_user** -> update_user(_api::UserApi, username::String, user::User; _mediaType=nothing) -> Nothing, OpenAPI.Clients.ApiResponse
-> update_user(_api::UserApi, response_stream::Channel, username::String, user::User; _mediaType=nothing) -> Channel{ Nothing }, OpenAPI.Clients.ApiResponse - -Updated user - -This can only be done by the logged in user. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **UserApi** | API context | -**username** | **String** | name that need to be deleted | -**user** | [**User**](User.md) | Updated user object | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - diff --git a/test/client/petstore_v3/petstore/src/PetStoreClient.jl b/test/client/petstore_v3/petstore/src/PetStoreClient.jl deleted file mode 100644 index e90d860..0000000 --- a/test/client/petstore_v3/petstore/src/PetStoreClient.jl +++ /dev/null @@ -1,31 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -module PetStoreClient - -using Dates, TimeZones -using OpenAPI -using OpenAPI.Clients - -const API_VERSION = "1.0.0" - -include("modelincludes.jl") - -include("apis/api_PetApi.jl") -include("apis/api_StoreApi.jl") -include("apis/api_UserApi.jl") - -# export models -export ApiResponse -export Category -export Order -export Pet -export Tag -export User - -# export operations -export PetApi -export StoreApi -export UserApi - -end # module PetStoreClient diff --git a/test/client/petstore_v3/petstore/src/apis/api_PetApi.jl b/test/client/petstore_v3/petstore/src/apis/api_PetApi.jl deleted file mode 100644 index 426eb55..0000000 --- a/test/client/petstore_v3/petstore/src/apis/api_PetApi.jl +++ /dev/null @@ -1,273 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -struct PetApi <: OpenAPI.APIClientImpl - client::OpenAPI.Clients.Client -end - -""" -The default API base path for APIs in `PetApi`. -This can be used to construct the `OpenAPI.Clients.Client` instance. -""" -basepath(::Type{ PetApi }) = "/v3" - -const _returntypes_add_pet_PetApi = Dict{Regex,Type}( - Regex("^" * replace("405", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_add_pet(_api::PetApi, pet::Pet; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_add_pet_PetApi, "/pet", ["petstore_auth", ], pet) - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", "application/xml", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Add a new pet to the store - -Params: -- pet::Pet (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function add_pet(_api::PetApi, pet::Pet; _mediaType=nothing) - _ctx = _oacinternal_add_pet(_api, pet; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function add_pet(_api::PetApi, response_stream::Channel, pet::Pet; _mediaType=nothing) - _ctx = _oacinternal_add_pet(_api, pet; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_delete_pet_PetApi = Dict{Regex,Type}( - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_delete_pet(_api::PetApi, pet_id::Int64; api_key=nothing, _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "DELETE", _returntypes_delete_pet_PetApi, "/pet/{petId}", ["petstore_auth", ]) - OpenAPI.Clients.set_param(_ctx.path, "petId", pet_id) # type Int64 - OpenAPI.Clients.set_param(_ctx.header, "api_key", api_key) # type String - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Deletes a pet - -Params: -- pet_id::Int64 (required) -- api_key::String - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function delete_pet(_api::PetApi, pet_id::Int64; api_key=nothing, _mediaType=nothing) - _ctx = _oacinternal_delete_pet(_api, pet_id; api_key=api_key, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function delete_pet(_api::PetApi, response_stream::Channel, pet_id::Int64; api_key=nothing, _mediaType=nothing) - _ctx = _oacinternal_delete_pet(_api, pet_id; api_key=api_key, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_find_pets_by_status_PetApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => Vector{Pet}, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_find_pets_by_status(_api::PetApi, status::Vector{String}; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_find_pets_by_status_PetApi, "/pet/findByStatus", ["petstore_auth", ]) - OpenAPI.Clients.set_param(_ctx.query, "status", status; style="form", is_explode=false) # type Vector{String} - OpenAPI.Clients.set_header_accept(_ctx, ["application/xml", "application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Finds Pets by status - -Multiple status values can be provided with comma separated strings - -Params: -- status::Vector{String} (required) - -Return: Vector{Pet}, OpenAPI.Clients.ApiResponse -""" -function find_pets_by_status(_api::PetApi, status::Vector{String}; _mediaType=nothing) - _ctx = _oacinternal_find_pets_by_status(_api, status; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function find_pets_by_status(_api::PetApi, response_stream::Channel, status::Vector{String}; _mediaType=nothing) - _ctx = _oacinternal_find_pets_by_status(_api, status; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_find_pets_by_tags_PetApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => Vector{Pet}, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_find_pets_by_tags(_api::PetApi, tags::Vector{String}; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_find_pets_by_tags_PetApi, "/pet/findByTags", ["petstore_auth", ]) - OpenAPI.Clients.set_param(_ctx.query, "tags", tags; style="form", is_explode=false) # type Vector{String} - OpenAPI.Clients.set_header_accept(_ctx, ["application/xml", "application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Finds Pets by tags - -Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. - -Params: -- tags::Vector{String} (required) - -Return: Vector{Pet}, OpenAPI.Clients.ApiResponse -""" -function find_pets_by_tags(_api::PetApi, tags::Vector{String}; _mediaType=nothing) - _ctx = _oacinternal_find_pets_by_tags(_api, tags; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function find_pets_by_tags(_api::PetApi, response_stream::Channel, tags::Vector{String}; _mediaType=nothing) - _ctx = _oacinternal_find_pets_by_tags(_api, tags; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_get_pet_by_id_PetApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => Pet, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_get_pet_by_id(_api::PetApi, pet_id::Int64; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_get_pet_by_id_PetApi, "/pet/{petId}", ["api_key", ]) - OpenAPI.Clients.set_param(_ctx.path, "petId", pet_id) # type Int64 - OpenAPI.Clients.set_header_accept(_ctx, ["application/xml", "application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Find pet by ID - -Returns a single pet - -Params: -- pet_id::Int64 (required) - -Return: Pet, OpenAPI.Clients.ApiResponse -""" -function get_pet_by_id(_api::PetApi, pet_id::Int64; _mediaType=nothing) - _ctx = _oacinternal_get_pet_by_id(_api, pet_id; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function get_pet_by_id(_api::PetApi, response_stream::Channel, pet_id::Int64; _mediaType=nothing) - _ctx = _oacinternal_get_pet_by_id(_api, pet_id; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_update_pet_PetApi = Dict{Regex,Type}( - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("405", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_update_pet(_api::PetApi, pet::Pet; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "PUT", _returntypes_update_pet_PetApi, "/pet", ["petstore_auth", ], pet) - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", "application/xml", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Update an existing pet - -Params: -- pet::Pet (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function update_pet(_api::PetApi, pet::Pet; _mediaType=nothing) - _ctx = _oacinternal_update_pet(_api, pet; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function update_pet(_api::PetApi, response_stream::Channel, pet::Pet; _mediaType=nothing) - _ctx = _oacinternal_update_pet(_api, pet; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_update_pet_with_form_PetApi = Dict{Regex,Type}( - Regex("^" * replace("405", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_update_pet_with_form(_api::PetApi, pet_id::Int64; name=nothing, status=nothing, _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_update_pet_with_form_PetApi, "/pet/{petId}", ["petstore_auth", ]) - OpenAPI.Clients.set_param(_ctx.path, "petId", pet_id) # type Int64 - OpenAPI.Clients.set_param(_ctx.form, "name", name) # type String - OpenAPI.Clients.set_param(_ctx.form, "status", status) # type String - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/x-www-form-urlencoded", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Updates a pet in the store with form data - -Params: -- pet_id::Int64 (required) -- name::String -- status::String - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function update_pet_with_form(_api::PetApi, pet_id::Int64; name=nothing, status=nothing, _mediaType=nothing) - _ctx = _oacinternal_update_pet_with_form(_api, pet_id; name=name, status=status, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function update_pet_with_form(_api::PetApi, response_stream::Channel, pet_id::Int64; name=nothing, status=nothing, _mediaType=nothing) - _ctx = _oacinternal_update_pet_with_form(_api, pet_id; name=name, status=status, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_upload_file_PetApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => ApiResponse, -) - -function _oacinternal_upload_file(_api::PetApi, pet_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_upload_file_PetApi, "/pet/{petId}/uploadImage", ["petstore_auth", ]) - OpenAPI.Clients.set_param(_ctx.path, "petId", pet_id) # type Int64 - OpenAPI.Clients.set_param(_ctx.form, "additionalMetadata", additional_metadata) # type String - OpenAPI.Clients.set_param(_ctx.file, "file", file) # type String - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["multipart/form-data", ] : [_mediaType]) - return _ctx -end - -@doc raw"""uploads an image - -Params: -- pet_id::Int64 (required) -- additional_metadata::String -- file::String - -Return: ApiResponse, OpenAPI.Clients.ApiResponse -""" -function upload_file(_api::PetApi, pet_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) - _ctx = _oacinternal_upload_file(_api, pet_id; additional_metadata=additional_metadata, file=file, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function upload_file(_api::PetApi, response_stream::Channel, pet_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) - _ctx = _oacinternal_upload_file(_api, pet_id; additional_metadata=additional_metadata, file=file, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -export add_pet -export delete_pet -export find_pets_by_status -export find_pets_by_tags -export get_pet_by_id -export update_pet -export update_pet_with_form -export upload_file diff --git a/test/client/petstore_v3/petstore/src/apis/api_StoreApi.jl b/test/client/petstore_v3/petstore/src/apis/api_StoreApi.jl deleted file mode 100644 index 250b09e..0000000 --- a/test/client/petstore_v3/petstore/src/apis/api_StoreApi.jl +++ /dev/null @@ -1,143 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -struct StoreApi <: OpenAPI.APIClientImpl - client::OpenAPI.Clients.Client -end - -""" -The default API base path for APIs in `StoreApi`. -This can be used to construct the `OpenAPI.Clients.Client` instance. -""" -basepath(::Type{ StoreApi }) = "/v3" - -const _returntypes_delete_order_StoreApi = Dict{Regex,Type}( - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_delete_order(_api::StoreApi, order_id::String; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "DELETE", _returntypes_delete_order_StoreApi, "/store/order/{orderId}", []) - OpenAPI.Clients.set_param(_ctx.path, "orderId", order_id) # type String - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Delete purchase order by ID - -For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors - -Params: -- order_id::String (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function delete_order(_api::StoreApi, order_id::String; _mediaType=nothing) - _ctx = _oacinternal_delete_order(_api, order_id; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function delete_order(_api::StoreApi, response_stream::Channel, order_id::String; _mediaType=nothing) - _ctx = _oacinternal_delete_order(_api, order_id; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_get_inventory_StoreApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => Dict{String, Int64}, -) - -function _oacinternal_get_inventory(_api::StoreApi; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_get_inventory_StoreApi, "/store/inventory", ["api_key", ]) - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Returns pet inventories by status - -Returns a map of status codes to quantities - -Params: - -Return: Dict{String, Int64}, OpenAPI.Clients.ApiResponse -""" -function get_inventory(_api::StoreApi; _mediaType=nothing) - _ctx = _oacinternal_get_inventory(_api; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function get_inventory(_api::StoreApi, response_stream::Channel; _mediaType=nothing) - _ctx = _oacinternal_get_inventory(_api; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_get_order_by_id_StoreApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => Order, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_get_order_by_id(_api::StoreApi, order_id::Int64; _mediaType=nothing) - OpenAPI.validate_param("order_id", "get_order_by_id", :maximum, order_id, 5, false) - OpenAPI.validate_param("order_id", "get_order_by_id", :minimum, order_id, 1, false) - - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_get_order_by_id_StoreApi, "/store/order/{orderId}", []) - OpenAPI.Clients.set_param(_ctx.path, "orderId", order_id) # type Int64 - OpenAPI.Clients.set_header_accept(_ctx, ["application/xml", "application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Find purchase order by ID - -For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions - -Params: -- order_id::Int64 (required) - -Return: Order, OpenAPI.Clients.ApiResponse -""" -function get_order_by_id(_api::StoreApi, order_id::Int64; _mediaType=nothing) - _ctx = _oacinternal_get_order_by_id(_api, order_id; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function get_order_by_id(_api::StoreApi, response_stream::Channel, order_id::Int64; _mediaType=nothing) - _ctx = _oacinternal_get_order_by_id(_api, order_id; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_place_order_StoreApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => Order, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_place_order(_api::StoreApi, order::Order; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_place_order_StoreApi, "/store/order", [], order) - OpenAPI.Clients.set_header_accept(_ctx, ["application/xml", "application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Place an order for a pet - -Params: -- order::Order (required) - -Return: Order, OpenAPI.Clients.ApiResponse -""" -function place_order(_api::StoreApi, order::Order; _mediaType=nothing) - _ctx = _oacinternal_place_order(_api, order; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function place_order(_api::StoreApi, response_stream::Channel, order::Order; _mediaType=nothing) - _ctx = _oacinternal_place_order(_api, order; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -export delete_order -export get_inventory -export get_order_by_id -export place_order diff --git a/test/client/petstore_v3/petstore/src/apis/api_UserApi.jl b/test/client/petstore_v3/petstore/src/apis/api_UserApi.jl deleted file mode 100644 index 5939776..0000000 --- a/test/client/petstore_v3/petstore/src/apis/api_UserApi.jl +++ /dev/null @@ -1,262 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -struct UserApi <: OpenAPI.APIClientImpl - client::OpenAPI.Clients.Client -end - -""" -The default API base path for APIs in `UserApi`. -This can be used to construct the `OpenAPI.Clients.Client` instance. -""" -basepath(::Type{ UserApi }) = "/v3" - -const _returntypes_create_user_UserApi = Dict{Regex,Type}( - Regex("^" * replace("0", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_create_user(_api::UserApi, user::User; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_create_user_UserApi, "/user", [], user) - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Create user - -This can only be done by the logged in user. - -Params: -- user::User (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function create_user(_api::UserApi, user::User; _mediaType=nothing) - _ctx = _oacinternal_create_user(_api, user; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function create_user(_api::UserApi, response_stream::Channel, user::User; _mediaType=nothing) - _ctx = _oacinternal_create_user(_api, user; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_create_users_with_array_input_UserApi = Dict{Regex,Type}( - Regex("^" * replace("0", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_create_users_with_array_input(_api::UserApi, user::Vector{User}; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_create_users_with_array_input_UserApi, "/user/createWithArray", [], user) - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Creates list of users with given input array - -Params: -- user::Vector{User} (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function create_users_with_array_input(_api::UserApi, user::Vector{User}; _mediaType=nothing) - _ctx = _oacinternal_create_users_with_array_input(_api, user; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function create_users_with_array_input(_api::UserApi, response_stream::Channel, user::Vector{User}; _mediaType=nothing) - _ctx = _oacinternal_create_users_with_array_input(_api, user; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_create_users_with_list_input_UserApi = Dict{Regex,Type}( - Regex("^" * replace("0", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_create_users_with_list_input(_api::UserApi, user::Vector{User}; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_create_users_with_list_input_UserApi, "/user/createWithList", [], user) - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Creates list of users with given input array - -Params: -- user::Vector{User} (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function create_users_with_list_input(_api::UserApi, user::Vector{User}; _mediaType=nothing) - _ctx = _oacinternal_create_users_with_list_input(_api, user; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function create_users_with_list_input(_api::UserApi, response_stream::Channel, user::Vector{User}; _mediaType=nothing) - _ctx = _oacinternal_create_users_with_list_input(_api, user; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_delete_user_UserApi = Dict{Regex,Type}( - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_delete_user(_api::UserApi, username::String; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "DELETE", _returntypes_delete_user_UserApi, "/user/{username}", []) - OpenAPI.Clients.set_param(_ctx.path, "username", username) # type String - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Delete user - -This can only be done by the logged in user. - -Params: -- username::String (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function delete_user(_api::UserApi, username::String; _mediaType=nothing) - _ctx = _oacinternal_delete_user(_api, username; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function delete_user(_api::UserApi, response_stream::Channel, username::String; _mediaType=nothing) - _ctx = _oacinternal_delete_user(_api, username; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_get_user_by_name_UserApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => User, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_get_user_by_name(_api::UserApi, username::String; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_get_user_by_name_UserApi, "/user/{username}", []) - OpenAPI.Clients.set_param(_ctx.path, "username", username) # type String - OpenAPI.Clients.set_header_accept(_ctx, ["application/xml", "application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Get user by user name - -Params: -- username::String (required) - -Return: User, OpenAPI.Clients.ApiResponse -""" -function get_user_by_name(_api::UserApi, username::String; _mediaType=nothing) - _ctx = _oacinternal_get_user_by_name(_api, username; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function get_user_by_name(_api::UserApi, response_stream::Channel, username::String; _mediaType=nothing) - _ctx = _oacinternal_get_user_by_name(_api, username; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_login_user_UserApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => String, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_login_user(_api::UserApi, username::String, password::String; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_login_user_UserApi, "/user/login", []) - OpenAPI.Clients.set_param(_ctx.query, "username", username; style="form", is_explode=true) # type String - OpenAPI.Clients.set_param(_ctx.query, "password", password; style="form", is_explode=true) # type String - OpenAPI.Clients.set_header_accept(_ctx, ["application/xml", "application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Logs user into the system - -Params: -- username::String (required) -- password::String (required) - -Return: String, OpenAPI.Clients.ApiResponse -""" -function login_user(_api::UserApi, username::String, password::String; _mediaType=nothing) - _ctx = _oacinternal_login_user(_api, username, password; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function login_user(_api::UserApi, response_stream::Channel, username::String, password::String; _mediaType=nothing) - _ctx = _oacinternal_login_user(_api, username, password; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_logout_user_UserApi = Dict{Regex,Type}( - Regex("^" * replace("0", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_logout_user(_api::UserApi; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_logout_user_UserApi, "/user/logout", []) - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Logs out current logged in user session - -Params: - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function logout_user(_api::UserApi; _mediaType=nothing) - _ctx = _oacinternal_logout_user(_api; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function logout_user(_api::UserApi, response_stream::Channel; _mediaType=nothing) - _ctx = _oacinternal_logout_user(_api; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_update_user_UserApi = Dict{Regex,Type}( - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, - Regex("^" * replace("404", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_update_user(_api::UserApi, username::String, user::User; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "PUT", _returntypes_update_user_UserApi, "/user/{username}", [], user) - OpenAPI.Clients.set_param(_ctx.path, "username", username) # type String - OpenAPI.Clients.set_header_accept(_ctx, []) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Updated user - -This can only be done by the logged in user. - -Params: -- username::String (required) -- user::User (required) - -Return: Nothing, OpenAPI.Clients.ApiResponse -""" -function update_user(_api::UserApi, username::String, user::User; _mediaType=nothing) - _ctx = _oacinternal_update_user(_api, username, user; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function update_user(_api::UserApi, response_stream::Channel, username::String, user::User; _mediaType=nothing) - _ctx = _oacinternal_update_user(_api, username, user; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -export create_user -export create_users_with_array_input -export create_users_with_list_input -export delete_user -export get_user_by_name -export login_user -export logout_user -export update_user diff --git a/test/client/petstore_v3/petstore/src/modelincludes.jl b/test/client/petstore_v3/petstore/src/modelincludes.jl deleted file mode 100644 index b3a3db8..0000000 --- a/test/client/petstore_v3/petstore/src/modelincludes.jl +++ /dev/null @@ -1,9 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -include("models/model_ApiResponse.jl") -include("models/model_Category.jl") -include("models/model_Order.jl") -include("models/model_Pet.jl") -include("models/model_Tag.jl") -include("models/model_User.jl") diff --git a/test/client/petstore_v3/petstore/src/models/model_ApiResponse.jl b/test/client/petstore_v3/petstore/src/models/model_ApiResponse.jl deleted file mode 100644 index 1d6b413..0000000 --- a/test/client/petstore_v3/petstore/src/models/model_ApiResponse.jl +++ /dev/null @@ -1,50 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""ApiResponse -Describes the result of uploading an image resource - - ApiResponse(; - code=nothing, - type=nothing, - message=nothing, - ) - - - code::Int64 - - type::String - - message::String -""" -Base.@kwdef mutable struct ApiResponse <: OpenAPI.APIModel - code::Union{Nothing, Int64} = nothing - type::Union{Nothing, String} = nothing - message::Union{Nothing, String} = nothing - - function ApiResponse(code, type, message, ) - o = new(code, type, message, ) - OpenAPI.validate_properties(o) - return o - end -end # type ApiResponse - -const _property_types_ApiResponse = Dict{Symbol,String}(Symbol("code")=>"Int64", Symbol("type")=>"String", Symbol("message")=>"String", ) -OpenAPI.property_type(::Type{ ApiResponse }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_ApiResponse[name]))} - -function OpenAPI.check_required(o::ApiResponse) - true -end - -function OpenAPI.validate_properties(o::ApiResponse) - OpenAPI.validate_property(ApiResponse, Symbol("code"), o.code) - OpenAPI.validate_property(ApiResponse, Symbol("type"), o.type) - OpenAPI.validate_property(ApiResponse, Symbol("message"), o.message) -end - -function OpenAPI.validate_property(::Type{ ApiResponse }, name::Symbol, val) - - if name === Symbol("code") - OpenAPI.validate_param(name, "ApiResponse", :format, val, "int32") - end - - -end diff --git a/test/client/petstore_v3/petstore/src/models/model_Category.jl b/test/client/petstore_v3/petstore/src/models/model_Category.jl deleted file mode 100644 index 843d8a5..0000000 --- a/test/client/petstore_v3/petstore/src/models/model_Category.jl +++ /dev/null @@ -1,45 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Category -A category for a pet - - Category(; - id=nothing, - name=nothing, - ) - - - id::Int64 - - name::String -""" -Base.@kwdef mutable struct Category <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - name::Union{Nothing, String} = nothing - - function Category(id, name, ) - o = new(id, name, ) - OpenAPI.validate_properties(o) - return o - end -end # type Category - -const _property_types_Category = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("name")=>"String", ) -OpenAPI.property_type(::Type{ Category }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Category[name]))} - -function OpenAPI.check_required(o::Category) - true -end - -function OpenAPI.validate_properties(o::Category) - OpenAPI.validate_property(Category, Symbol("id"), o.id) - OpenAPI.validate_property(Category, Symbol("name"), o.name) -end - -function OpenAPI.validate_property(::Type{ Category }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "Category", :format, val, "int64") - end - -end diff --git a/test/client/petstore_v3/petstore/src/models/model_Order.jl b/test/client/petstore_v3/petstore/src/models/model_Order.jl deleted file mode 100644 index c1e95bc..0000000 --- a/test/client/petstore_v3/petstore/src/models/model_Order.jl +++ /dev/null @@ -1,78 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Order -An order for a pets from the pet store - - Order(; - id=nothing, - petId=nothing, - quantity=nothing, - shipDate=nothing, - status=nothing, - complete=false, - ) - - - id::Int64 - - petId::Int64 - - quantity::Int64 - - shipDate::ZonedDateTime - - status::String : Order Status - - complete::Bool -""" -Base.@kwdef mutable struct Order <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - petId::Union{Nothing, Int64} = nothing - quantity::Union{Nothing, Int64} = nothing - shipDate::Union{Nothing, ZonedDateTime} = nothing - status::Union{Nothing, String} = nothing - complete::Union{Nothing, Bool} = false - - function Order(id, petId, quantity, shipDate, status, complete, ) - o = new(id, petId, quantity, shipDate, status, complete, ) - OpenAPI.validate_properties(o) - return o - end -end # type Order - -const _property_types_Order = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("petId")=>"Int64", Symbol("quantity")=>"Int64", Symbol("shipDate")=>"ZonedDateTime", Symbol("status")=>"String", Symbol("complete")=>"Bool", ) -OpenAPI.property_type(::Type{ Order }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Order[name]))} - -function OpenAPI.check_required(o::Order) - true -end - -function OpenAPI.validate_properties(o::Order) - OpenAPI.validate_property(Order, Symbol("id"), o.id) - OpenAPI.validate_property(Order, Symbol("petId"), o.petId) - OpenAPI.validate_property(Order, Symbol("quantity"), o.quantity) - OpenAPI.validate_property(Order, Symbol("shipDate"), o.shipDate) - OpenAPI.validate_property(Order, Symbol("status"), o.status) - OpenAPI.validate_property(Order, Symbol("complete"), o.complete) -end - -function OpenAPI.validate_property(::Type{ Order }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "Order", :format, val, "int64") - end - - if name === Symbol("petId") - OpenAPI.validate_param(name, "Order", :format, val, "int64") - end - - if name === Symbol("quantity") - OpenAPI.validate_param(name, "Order", :format, val, "int32") - end - - if name === Symbol("shipDate") - OpenAPI.validate_param(name, "Order", :format, val, "date-time") - end - - if name === Symbol("status") - OpenAPI.validate_param(name, "Order", :enum, val, ["placed", "approved", "delivered"]) - end - - -end diff --git a/test/client/petstore_v3/petstore/src/models/model_Pet.jl b/test/client/petstore_v3/petstore/src/models/model_Pet.jl deleted file mode 100644 index 47f8f83..0000000 --- a/test/client/petstore_v3/petstore/src/models/model_Pet.jl +++ /dev/null @@ -1,71 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Pet -A pet for sale in the pet store - - Pet(; - id=nothing, - category=nothing, - name=nothing, - photoUrls=nothing, - tags=nothing, - status=nothing, - ) - - - id::Int64 - - category::Category - - name::String - - photoUrls::Vector{String} - - tags::Vector{Tag} - - status::String : pet status in the store -""" -Base.@kwdef mutable struct Pet <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - category = nothing # spec type: Union{ Nothing, Category } - name::Union{Nothing, String} = nothing - photoUrls::Union{Nothing, Vector{String}} = nothing - tags::Union{Nothing, Vector} = nothing # spec type: Union{ Nothing, Vector{Tag} } - status::Union{Nothing, String} = nothing - - function Pet(id, category, name, photoUrls, tags, status, ) - o = new(id, category, name, photoUrls, tags, status, ) - OpenAPI.validate_properties(o) - return o - end -end # type Pet - -const _property_types_Pet = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("category")=>"Category", Symbol("name")=>"String", Symbol("photoUrls")=>"Vector{String}", Symbol("tags")=>"Vector{Tag}", Symbol("status")=>"String", ) -OpenAPI.property_type(::Type{ Pet }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Pet[name]))} - -function OpenAPI.check_required(o::Pet) - o.name === nothing && (return false) - o.photoUrls === nothing && (return false) - true -end - -function OpenAPI.validate_properties(o::Pet) - OpenAPI.validate_property(Pet, Symbol("id"), o.id) - OpenAPI.validate_property(Pet, Symbol("category"), o.category) - OpenAPI.validate_property(Pet, Symbol("name"), o.name) - OpenAPI.validate_property(Pet, Symbol("photoUrls"), o.photoUrls) - OpenAPI.validate_property(Pet, Symbol("tags"), o.tags) - OpenAPI.validate_property(Pet, Symbol("status"), o.status) -end - -function OpenAPI.validate_property(::Type{ Pet }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "Pet", :format, val, "int64") - end - - - - - - if name === Symbol("status") - OpenAPI.validate_param(name, "Pet", :enum, val, ["available", "pending", "sold"]) - end - -end diff --git a/test/client/petstore_v3/petstore/src/models/model_Tag.jl b/test/client/petstore_v3/petstore/src/models/model_Tag.jl deleted file mode 100644 index 2743b59..0000000 --- a/test/client/petstore_v3/petstore/src/models/model_Tag.jl +++ /dev/null @@ -1,45 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Tag -A tag for a pet - - Tag(; - id=nothing, - name=nothing, - ) - - - id::Int64 - - name::String -""" -Base.@kwdef mutable struct Tag <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - name::Union{Nothing, String} = nothing - - function Tag(id, name, ) - o = new(id, name, ) - OpenAPI.validate_properties(o) - return o - end -end # type Tag - -const _property_types_Tag = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("name")=>"String", ) -OpenAPI.property_type(::Type{ Tag }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Tag[name]))} - -function OpenAPI.check_required(o::Tag) - true -end - -function OpenAPI.validate_properties(o::Tag) - OpenAPI.validate_property(Tag, Symbol("id"), o.id) - OpenAPI.validate_property(Tag, Symbol("name"), o.name) -end - -function OpenAPI.validate_property(::Type{ Tag }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "Tag", :format, val, "int64") - end - -end diff --git a/test/client/petstore_v3/petstore/src/models/model_User.jl b/test/client/petstore_v3/petstore/src/models/model_User.jl deleted file mode 100644 index 8e95c9a..0000000 --- a/test/client/petstore_v3/petstore/src/models/model_User.jl +++ /dev/null @@ -1,78 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""User -A User who is purchasing from the pet store - - User(; - id=nothing, - username=nothing, - firstName=nothing, - lastName=nothing, - email=nothing, - password=nothing, - phone=nothing, - userStatus=nothing, - ) - - - id::Int64 - - username::String - - firstName::String - - lastName::String - - email::String - - password::String - - phone::String - - userStatus::Int64 : User Status -""" -Base.@kwdef mutable struct User <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - username::Union{Nothing, String} = nothing - firstName::Union{Nothing, String} = nothing - lastName::Union{Nothing, String} = nothing - email::Union{Nothing, String} = nothing - password::Union{Nothing, String} = nothing - phone::Union{Nothing, String} = nothing - userStatus::Union{Nothing, Int64} = nothing - - function User(id, username, firstName, lastName, email, password, phone, userStatus, ) - o = new(id, username, firstName, lastName, email, password, phone, userStatus, ) - OpenAPI.validate_properties(o) - return o - end -end # type User - -const _property_types_User = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("username")=>"String", Symbol("firstName")=>"String", Symbol("lastName")=>"String", Symbol("email")=>"String", Symbol("password")=>"String", Symbol("phone")=>"String", Symbol("userStatus")=>"Int64", ) -OpenAPI.property_type(::Type{ User }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_User[name]))} - -function OpenAPI.check_required(o::User) - true -end - -function OpenAPI.validate_properties(o::User) - OpenAPI.validate_property(User, Symbol("id"), o.id) - OpenAPI.validate_property(User, Symbol("username"), o.username) - OpenAPI.validate_property(User, Symbol("firstName"), o.firstName) - OpenAPI.validate_property(User, Symbol("lastName"), o.lastName) - OpenAPI.validate_property(User, Symbol("email"), o.email) - OpenAPI.validate_property(User, Symbol("password"), o.password) - OpenAPI.validate_property(User, Symbol("phone"), o.phone) - OpenAPI.validate_property(User, Symbol("userStatus"), o.userStatus) -end - -function OpenAPI.validate_property(::Type{ User }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "User", :format, val, "int64") - end - - - - - - - - if name === Symbol("userStatus") - OpenAPI.validate_param(name, "User", :format, val, "int32") - end -end diff --git a/test/client/petstore_v3/petstore_test_petapi.jl b/test/client/petstore_v3/petstore_test_petapi.jl deleted file mode 100644 index d139d7b..0000000 --- a/test/client/petstore_v3/petstore_test_petapi.jl +++ /dev/null @@ -1,77 +0,0 @@ -module TestPetApi - -using ..PetStoreClient -using Test -using OpenAPI -using OpenAPI.Clients -import OpenAPI.Clients: Client - -function test(uri, httplib::Symbol; test_file_upload=false) - @info("PetApi ($httplib backend)") - client = Client(uri; httplib=httplib) - api = PetApi(client) - - tag1 = Tag(;id=10, name="juliacat") - tag2 = Tag(;id=11, name="white") - cat = Category(;id=10, name="cat") - - @test_throws OpenAPI.ValidationException Pet(;id=10, category=cat, name="felix", photoUrls=nothing, tags=[tag1, tag2], status="invalid-status") - - pet = Pet(;id=10, category=cat, name="felix", photoUrls=["http://photo/1","http://photo/2"], tags=[tag1,tag2], status="pending") - - @info("PetApi - add_pet") - api_return, http_resp = add_pet(api, pet) - @test api_return === nothing - @test http_resp.status == 200 - - @info("PetApi - update_pet") - pet.status = "available" - api_return, http_resp = update_pet(api, pet) - @test api_return === nothing - @test http_resp.status == 200 - - # @info("PetApi - update_pet_with_form") - # @test update_pet_with_form(api, 10; in_name="meow") === nothing - - @info("PetApi - get_pet_by_id") - pet10, http_resp = get_pet_by_id(api, Int64(10)) - @test pet10.id == 10 - @test http_resp.status == 200 - - @info("PetApi - find_pets_by_status") - unsold = ["available", "pending"] - pets, http_resp = find_pets_by_status(api, unsold) - @test isa(pets, Vector{Pet}) - @test http_resp.status == 200 - @info("PetApi - find_pets_by_status", npets=length(pets)) - for p in pets - @test p.status in unsold - end - - @info("PetApi - delete_pet") - api_return, http_resp = delete_pet(api, Int64(10)) - @test api_return === nothing - @test http_resp.status == 200 - - if test_file_upload - @info("PetApi - upload_file") - api_return, http_resp = upload_file(api, 1; additional_metadata="my metadata", file=@__FILE__) - @test isa(api_return, ApiResponse) - @test api_return.code == 1 - @test api_return.type == "pet" - @test api_return.message == "file uploaded" - @test http_resp.status == 200 - end - - # does not work yet. issue: https://github.com/JuliaWeb/Requests.jl/issues/139 - #@info("PetApi - upload_file") - #img = joinpath(dirname(@__FILE__), "cat.png") - #resp, http_resp = upload_file(api, 10; additionalMetadata="juliacat pic", file=img) - #@test isa(resp, ApiResponse) - #@test resp.code == 200 - #@info("PetApi - upload_file", typ=get_field(resp, "type"), message=get_field(resp, "message")) - - nothing -end - -end # module TestPetApi diff --git a/test/client/petstore_v3/petstore_test_storeapi.jl b/test/client/petstore_v3/petstore_test_storeapi.jl deleted file mode 100644 index 552b5ad..0000000 --- a/test/client/petstore_v3/petstore_test_storeapi.jl +++ /dev/null @@ -1,74 +0,0 @@ -module TestStoreApi - -using ..PetStoreClient -using Test -using Dates -using TimeZones -using OpenAPI -using OpenAPI.Clients -import OpenAPI.Clients: Client - -function test(uri, httplib::Symbol) - @info("StoreApi ($httplib backend)") - client = Client(uri; httplib=httplib) - api = StoreApi(client) - - @info("StoreApi - get_inventory") - inventory, http_resp = get_inventory(api) - @test http_resp.status == 200 - @test isa(inventory, Dict{String,Int64}) - @test !isempty(inventory) - - @info("StoreApi - place_order") - @test_throws OpenAPI.ValidationException Order(; id=5, petId=10, quantity=2, shipDate=ZonedDateTime(DateTime(2017, 03, 12), localzone()), status="invalid_status", complete=false) - order = Order(; id=5, petId=10, quantity=2, shipDate=ZonedDateTime(DateTime(2017, 03, 12), localzone()), status="placed", complete=false) - neworder, http_resp = place_order(api, order) - @test http_resp.status == 200 - @test neworder.id == 5 - - @info("StoreApi - get_order_by_id") - @test_throws OpenAPI.ValidationException get_order_by_id(api, Int64(0)) - order, http_resp = get_order_by_id(api, Int64(5)) - @test http_resp.status == 200 - @test isa(order, Order) - @test order.id == 5 - @test isa(order.shipDate, ZonedDateTime) - - @info("StoreApi - get_order_by_id (async)") - response_channel = Channel{Order}(1) - @test_throws OpenAPI.ValidationException get_order_by_id(api, response_channel, Int64(0)) - @sync begin - @async begin - api_return, http_resp = get_order_by_id(api, response_channel, Int64(5)) - @test (200 <= http_resp.status <= 206) - @test api_return === response_channel - end - @async begin - order = take!(response_channel) - @test isa(order, Order) - @test order.id == 5 - end - end - - # a closed channel is equivalent of cancellation of the call, - # no error should be thrown, but response can be nothing if call was interrupted immediately - @test !isopen(response_channel) - - # open a new channel to use - response_channel = Channel{Order}(1) - try - resp, http_resp = get_order_by_id(api, response_channel, Int64(5)) - @test (200 <= http_resp.status <= 206) - catch ex - @test isa(ex, OpenAPI.InvocationException) - end - - @info("StoreApi - delete_order") - api_return, http_resp = delete_order(api, "5") - @test api_return === nothing - @test http_resp.status == 200 - - nothing -end - -end # module TestStoreApi diff --git a/test/client/petstore_v3/petstore_test_userapi.jl b/test/client/petstore_v3/petstore_test_userapi.jl deleted file mode 100644 index 49d3279..0000000 --- a/test/client/petstore_v3/petstore_test_userapi.jl +++ /dev/null @@ -1,193 +0,0 @@ -module TestUserApi - -using ..PetStoreClient -using Test -using Random -using JSON -using URIs -using OpenAPI -using OpenAPI.Clients -import OpenAPI.Clients: Client, Ctx, ApiException, DEFAULT_TIMEOUT_SECS, with_timeout, set_timeout, set_user_agent, set_cookie - -const TEST_USER = "jloac" -const TEST_USER1 = "jloac1" -const TEST_USER2 = "jloac2" -const TEST_USER3 = "jl oac 3" -const PRESET_TEST_USER = "user1" # this is the username that works for get user requests (as documented in the test docker container API) - -function test_404(uri, httplib::Symbol) - @info("Error handling ($httplib backend)") - client = Client(uri*"/invalid"; httplib=httplib) - api = UserApi(client) - - api_return, http_resp = login_user(api, TEST_USER, "testpassword") - @test api_return === nothing - @test http_resp.status == 404 - - client = Client("http://_invalid/"; httplib=httplib) - api = UserApi(client) - - try - login_user(api, TEST_USER, "testpassword") - @error("ApiException not thrown") - catch ex - @test isa(ex, ApiException) - @test startswith(ex.reason, "Could not resolve host") || startswith(ex.reason, "DNSError") - end -end - -function test_set_methods() - @info("Error handling") - client = Client("http://_invalid/") - - @test client.timeout[] == DEFAULT_TIMEOUT_SECS - - with_timeout(client, DEFAULT_TIMEOUT_SECS + 10) do client - @test client.timeout[] == DEFAULT_TIMEOUT_SECS + 10 - end - @test client.timeout[] == DEFAULT_TIMEOUT_SECS - - api = UserApi(client) - with_timeout(api, DEFAULT_TIMEOUT_SECS + 10) do api - @test api.client.timeout[] == DEFAULT_TIMEOUT_SECS + 10 - end - @test client.timeout[] == DEFAULT_TIMEOUT_SECS - - set_timeout(client, DEFAULT_TIMEOUT_SECS + 10) - @test client.timeout[] == DEFAULT_TIMEOUT_SECS + 10 - - @test isempty(client.headers) - set_user_agent(client, "007") - set_cookie(client, "crumbly") - @test client.headers["User-Agent"] == "007" - @test client.headers["Cookie"] == "crumbly" -end - -function test_login_user_hook(ctx::Ctx) - ctx.header["actual_password"] = "testpassword" - ctx -end - -function test_login_user_hook(resource_path::AbstractString, body::Any, headers::Dict{String,String}) - uri = URIs.parse_uri(resource_path) - qparams = URIs.queryparams(uri) - qparams["password"] = headers["actual_password"] - delete!(headers, "actual_password") - resource_path = string(URIs.URI(uri; query=escapeuri(qparams))) - - (resource_path, body, headers) -end - -function test_userhook(uri, httplib::Symbol) - @info("User hook ($httplib backend)") - client = Client(uri; pre_request_hook=test_login_user_hook, httplib=httplib) - api = UserApi(client) - - login_result, http_resp = login_user(api, TEST_USER, "wrongpassword") - @test http_resp.status == 200 - @test !isempty(login_result) - @test startswith(login_result, "logged in user session:") -end - -function test_parallel(uri, httplib::Symbol) - @info("Parallel usage ($httplib backend)") - client = Client(uri; httplib=httplib) - api = UserApi(client) - - for gcidx in 1:100 - @sync begin - for idx in 1:10^3 - @async begin - @debug("[$idx] UserApi Parallel begin") - login_result, http_resp = login_user(api, TEST_USER, "testpassword") - @test http_resp.status == 200 - @test !isempty(login_result) - @test startswith(login_result, "logged in user session:") - - @test_throws ApiException get_user_by_name(api, randstring()) - @test_throws ApiException get_user_by_name(api, TEST_USER) - - logout_result, http_resp = logout_user(api) - @test http_resp.status == 200 - @test logout_result === nothing - @debug("[$idx] UserApi Parallel end") - end - end - end - GC.gc() - @info("outer loop $gcidx") - end - nothing -end - -function test(uri, httplib::Symbol) - @info("UserApi ($httplib backend)") - client = Client(uri) - api = UserApi(client) - - @info("UserApi - login_user") - login_result, http_resp = login_user(api, TEST_USER, "testpassword") - @test http_resp.status == 200 - @test !isempty(login_result) - - @info("UserApi - create_user") - user1 = User(; id=100, username=TEST_USER1, firstName="test1", lastName="user1", email="jloac1@example.com", password="testpass1", phone="1000000001", userStatus=0) - create_result, http_resp = create_user(api, user1) - @test http_resp.status == 200 - @test create_result === nothing - - @info("UserApi - create_users_with_array_input") - user2 = User(; id=200, username=TEST_USER2, firstName="test2", lastName="user2", email="jloac2@example.com", password="testpass2", phone="1000000002", userStatus=0) - create_result, http_resp = create_users_with_array_input(api, [user1, user2]) - @test http_resp.status == 200 - @test create_result === nothing - - @info("UserApi - create_users_with_array_input") - create_result, http_resp = create_users_with_array_input(api, [user1, user2]) - @test http_resp.status == 200 - @test create_result === nothing - - @info("UserApi - get_user_by_name") - getuser_result, http_resp = get_user_by_name(api, randstring()) - @test http_resp.status == 404 - @test nothing === getuser_result - getuser_result, http_resp = get_user_by_name(api, TEST_USER) - @test http_resp.status == 404 - @test nothing === getuser_result - getuser_result, http_resp = get_user_by_name(api, PRESET_TEST_USER) - @test http_resp.status == 200 - @test isa(getuser_result, User) - - @info("UserApi - update_user") - api_return, http_resp = update_user(api, TEST_USER2, getuser_result) - @test http_resp.status == 200 - @test api_return === nothing - @info("UserApi - delete_user") - api_return, http_resp = delete_user(api, TEST_USER2) - @test http_resp.status == 200 - @test api_return === nothing - - @info("UserApi - logout_user") - logout_result, http_resp = logout_user(api) - @test http_resp.status == 200 - @test logout_result === nothing - - @info("UserApi - Test with spaces in username") - user3 = User(; id=300, username=TEST_USER3, firstName="test3", lastName="user3", email="jloac3@example.com", password="testpass3", phone="1000000003", userStatus=0) - create_result, http_resp = create_user(api, user3) - @test http_resp.status == 200 - @test create_result === nothing - - user3.firstName = "test3 updated" - api_return, http_resp = update_user(api, TEST_USER3, user3) - @test http_resp.status == 200 - @test api_return === nothing - - api_return, http_resp = delete_user(api, TEST_USER3) - @test http_resp.status == 200 - @test api_return === nothing - - nothing -end - -end # module TestUserApi diff --git a/test/client/petstore_v3/runtests.jl b/test/client/petstore_v3/runtests.jl deleted file mode 100644 index 7f01a5c..0000000 --- a/test/client/petstore_v3/runtests.jl +++ /dev/null @@ -1,44 +0,0 @@ -module PetStoreV3Tests - -include(joinpath(@__DIR__, "petstore", "src", "PetStoreClient.jl")) -using .PetStoreClient -using Test - -include("petstore_test_petapi.jl") -include("petstore_test_userapi.jl") -include("petstore_test_storeapi.jl") - -const server = "http://127.0.0.1:8081/v3" - -function test_misc(httplib::Symbol) - TestUserApi.test_404(server, httplib) - TestUserApi.test_userhook(server, httplib) - TestUserApi.test_set_methods() -end - -function test_stress(httplib::Symbol) - TestUserApi.test_parallel(server, httplib) -end - -function petstore_tests(httplib::Symbol; test_file_upload=false) - TestUserApi.test(server, httplib) - TestStoreApi.test(server, httplib) - TestPetApi.test(server, httplib; test_file_upload=test_file_upload) -end - -function runtests(httplib::Symbol; test_file_upload=false) - @testset "petstore v3" begin - @testset "miscellaneous" begin - test_misc(httplib) - end - @testset "petstore apis" begin - petstore_tests(httplib; test_file_upload=test_file_upload) - end - if get(ENV, "STRESS_PETSTORE", "false") == "true" - @testset "stress" begin - test_stress(httplib) - end - end - end -end -end # module PetStoreV3Tests diff --git a/test/client/petstore_v3/start_petstore_server.sh b/test/client/petstore_v3/start_petstore_server.sh deleted file mode 100755 index 9c6d4fe..0000000 --- a/test/client/petstore_v3/start_petstore_server.sh +++ /dev/null @@ -1,4 +0,0 @@ -docker stop openapi-petstore 2> /dev/null -docker rm openapi-petstore 2> /dev/null -docker pull openapitools/openapi-petstore:latest -docker run --rm -d --name openapi-petstore -e OPENAPI_BASE_PATH=/v3 -e DISABLE_API_KEY=1 -e DISABLE_OAUTH=1 -p 8081:8080 openapitools/openapi-petstore diff --git a/test/client/petstore_v3/stop_petstore_server.sh b/test/client/petstore_v3/stop_petstore_server.sh deleted file mode 100755 index 957fb24..0000000 --- a/test/client/petstore_v3/stop_petstore_server.sh +++ /dev/null @@ -1,4 +0,0 @@ -echo "stopping openapi-petstore server" -docker stop openapi-petstore -docker rm openapi-petstore 2>/dev/null -echo "stopped openapi-petstore server" \ No newline at end of file diff --git a/test/client/runtests.jl b/test/client/runtests.jl deleted file mode 100644 index 16ee164..0000000 --- a/test/client/runtests.jl +++ /dev/null @@ -1,58 +0,0 @@ -module OpenAPIClientTests - -using OpenAPI -using OpenAPI.Clients -using Test - -include("utilstests.jl") -include("petstore_v3/runtests.jl") -include("petstore_v2/runtests.jl") -include("openapigenerator_petstore_v3/runtests.jl") - -function runtests(httplib::Symbol; skip_petstore=false, test_file_upload=false) - @testset "Client" begin - @testset "deepObj query param serialization" begin - include("client/param_serialize.jl") - end - @testset "Utils" begin - test_longpoll_exception_check() - test_request_interrupted_exception_check() - test_date() - test_misc() - test_has_property() - test_storefile() - end - @testset "Validations" begin - test_validations() - end - if !skip_petstore - @testset "Petstore" begin - if get(ENV, "RUNNER_OS", "") == "Linux" - @testset "V3" begin - @info("Running petstore v3 tests") - PetStoreV3Tests.runtests(httplib; test_file_upload=test_file_upload) - end - @testset "V2" begin - @info("Running petstore v2 tests") - PetStoreV2Tests.runtests(httplib) - end - else - @info("Skipping petstore tests in non Linux environment (can not run petstore docker on OSX or Windows)") - end - end - end - end -end - -function run_openapigenerator_tests(httplib::Symbol; test_file_upload=false) - @testset "OpenAPIGeneratorPetstoreClient" begin - if get(ENV, "RUNNER_OS", "") == "Linux" - @info("Running petstore v3 tests ($httplib backend)") - OpenAPIGenPetStoreV3Tests.runtests(httplib; test_file_upload=test_file_upload) - else - @info("Skipping petstore tests in non Linux environment (can not run petstore docker on OSX or Windows)") - end - end -end - -end # module OpenAPIClientTests diff --git a/test/client/timeouttest/TimeoutTestClient/.openapi-generator-ignore b/test/client/timeouttest/TimeoutTestClient/.openapi-generator-ignore deleted file mode 100644 index 7484ee5..0000000 --- a/test/client/timeouttest/TimeoutTestClient/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/test/client/timeouttest/TimeoutTestClient/.openapi-generator/FILES b/test/client/timeouttest/TimeoutTestClient/.openapi-generator/FILES deleted file mode 100644 index 7d2bf6b..0000000 --- a/test/client/timeouttest/TimeoutTestClient/.openapi-generator/FILES +++ /dev/null @@ -1,7 +0,0 @@ -README.md -docs/DefaultApi.md -docs/DelayresponseGet200Response.md -src/TimeoutTestClient.jl -src/apis/api_DefaultApi.jl -src/modelincludes.jl -src/models/model_DelayresponseGet200Response.jl diff --git a/test/client/timeouttest/TimeoutTestClient/.openapi-generator/VERSION b/test/client/timeouttest/TimeoutTestClient/.openapi-generator/VERSION deleted file mode 100644 index 4c631cf..0000000 --- a/test/client/timeouttest/TimeoutTestClient/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.14.0-SNAPSHOT diff --git a/test/client/timeouttest/TimeoutTestClient/README.md b/test/client/timeouttest/TimeoutTestClient/README.md deleted file mode 100644 index a210913..0000000 --- a/test/client/timeouttest/TimeoutTestClient/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Julia API client for TimeoutTestClient - -No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - -## Overview -This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. - -- API version: 1.0.0 -- Generator version: 7.14.0-SNAPSHOT -- Build package: org.openapitools.codegen.languages.JuliaClientCodegen - - -## Installation -Place the Julia files generated under the `src` folder in your Julia project. Include TimeoutTestClient.jl in the project code. -It would include the module named TimeoutTestClient. - -Documentation is generated as markdown files under the `docs` folder. You can include them in your project documentation. -Documentation is also embedded in Julia which can be used with a Julia specific documentation generator. - -## API Endpoints - -Class | Method ------------- | ------------- -*DefaultApi* | [**delayresponse_get**](docs/DefaultApi.md#delayresponse_get)
**GET** /delayresponse
Delay Response Endpoint -*DefaultApi* | [**longpollstream_get**](docs/DefaultApi.md#longpollstream_get)
**GET** /longpollstream
Long polled streaming endpoint - - -## Models - - - [DelayresponseGet200Response](docs/DelayresponseGet200Response.md) - - - -## Authorization -Endpoints do not require authorization. - - -## Author - - - diff --git a/test/client/timeouttest/TimeoutTestClient/docs/DefaultApi.md b/test/client/timeouttest/TimeoutTestClient/docs/DefaultApi.md deleted file mode 100644 index edc11a8..0000000 --- a/test/client/timeouttest/TimeoutTestClient/docs/DefaultApi.md +++ /dev/null @@ -1,66 +0,0 @@ -# DefaultApi - -All URIs are relative to *http://localhost* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**delayresponse_get**](DefaultApi.md#delayresponse_get) | **GET** /delayresponse | Delay Response Endpoint -[**longpollstream_get**](DefaultApi.md#longpollstream_get) | **GET** /longpollstream | Long polled streaming endpoint - - -# **delayresponse_get** -> delayresponse_get(_api::DefaultApi, delay_seconds::Int64; _mediaType=nothing) -> DelayresponseGet200Response, OpenAPI.Clients.ApiResponse
-> delayresponse_get(_api::DefaultApi, response_stream::Channel, delay_seconds::Int64; _mediaType=nothing) -> Channel{ DelayresponseGet200Response }, OpenAPI.Clients.ApiResponse - -Delay Response Endpoint - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **DefaultApi** | API context | -**delay_seconds** | **Int64** | Number of seconds to delay the response | - -### Return type - -[**DelayresponseGet200Response**](DelayresponseGet200Response.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **longpollstream_get** -> longpollstream_get(_api::DefaultApi, delay_seconds::Int64; _mediaType=nothing) -> DelayresponseGet200Response, OpenAPI.Clients.ApiResponse
-> longpollstream_get(_api::DefaultApi, response_stream::Channel, delay_seconds::Int64; _mediaType=nothing) -> Channel{ DelayresponseGet200Response }, OpenAPI.Clients.ApiResponse - -Long polled streaming endpoint - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **DefaultApi** | API context | -**delay_seconds** | **Int64** | Number of seconds to delay the response | - -### Return type - -[**DelayresponseGet200Response**](DelayresponseGet200Response.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - diff --git a/test/client/timeouttest/TimeoutTestClient/docs/DelayresponseGet200Response.md b/test/client/timeouttest/TimeoutTestClient/docs/DelayresponseGet200Response.md deleted file mode 100644 index b47de34..0000000 --- a/test/client/timeouttest/TimeoutTestClient/docs/DelayresponseGet200Response.md +++ /dev/null @@ -1,12 +0,0 @@ -# DelayresponseGet200Response - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**delay_seconds** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/client/timeouttest/TimeoutTestClient/src/TimeoutTestClient.jl b/test/client/timeouttest/TimeoutTestClient/src/TimeoutTestClient.jl deleted file mode 100644 index 936725b..0000000 --- a/test/client/timeouttest/TimeoutTestClient/src/TimeoutTestClient.jl +++ /dev/null @@ -1,16 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -module TimeoutTestClient - -using Dates, TimeZones -using OpenAPI -using OpenAPI.Clients - -const API_VERSION = "1.0.0" - -include("modelincludes.jl") - -include("apis/api_DefaultApi.jl") - -end # module TimeoutTestClient diff --git a/test/client/timeouttest/TimeoutTestClient/src/apis/api_DefaultApi.jl b/test/client/timeouttest/TimeoutTestClient/src/apis/api_DefaultApi.jl deleted file mode 100644 index 8e68f2a..0000000 --- a/test/client/timeouttest/TimeoutTestClient/src/apis/api_DefaultApi.jl +++ /dev/null @@ -1,77 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -struct DefaultApi <: OpenAPI.APIClientImpl - client::OpenAPI.Clients.Client -end - -""" -The default API base path for APIs in `DefaultApi`. -This can be used to construct the `OpenAPI.Clients.Client` instance. -""" -basepath(::Type{ DefaultApi }) = "http://localhost" - -const _returntypes_delayresponse_get_DefaultApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => DelayresponseGet200Response, -) - -function _oacinternal_delayresponse_get(_api::DefaultApi, delay_seconds::Int64; _mediaType=nothing) - OpenAPI.validate_param("delay_seconds", "delayresponse_get", :minimum, delay_seconds, 0, false) - - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_delayresponse_get_DefaultApi, "/delayresponse", []) - OpenAPI.Clients.set_param(_ctx.query, "delay_seconds", delay_seconds; style="form", is_explode=true) # type Int64 - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Delay Response Endpoint - -Params: -- delay_seconds::Int64 (required) - -Return: DelayresponseGet200Response, OpenAPI.Clients.ApiResponse -""" -function delayresponse_get(_api::DefaultApi, delay_seconds::Int64; _mediaType=nothing) - _ctx = _oacinternal_delayresponse_get(_api, delay_seconds; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function delayresponse_get(_api::DefaultApi, response_stream::Channel, delay_seconds::Int64; _mediaType=nothing) - _ctx = _oacinternal_delayresponse_get(_api, delay_seconds; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_longpollstream_get_DefaultApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => DelayresponseGet200Response, -) - -function _oacinternal_longpollstream_get(_api::DefaultApi, delay_seconds::Int64; _mediaType=nothing) - OpenAPI.validate_param("delay_seconds", "longpollstream_get", :minimum, delay_seconds, 0, false) - - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_longpollstream_get_DefaultApi, "/longpollstream", []) - OpenAPI.Clients.set_param(_ctx.query, "delay_seconds", delay_seconds; style="form", is_explode=true) # type Int64 - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Long polled streaming endpoint - -Params: -- delay_seconds::Int64 (required) - -Return: DelayresponseGet200Response, OpenAPI.Clients.ApiResponse -""" -function longpollstream_get(_api::DefaultApi, delay_seconds::Int64; _mediaType=nothing) - _ctx = _oacinternal_longpollstream_get(_api, delay_seconds; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function longpollstream_get(_api::DefaultApi, response_stream::Channel, delay_seconds::Int64; _mediaType=nothing) - _ctx = _oacinternal_longpollstream_get(_api, delay_seconds; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -export delayresponse_get -export longpollstream_get diff --git a/test/client/timeouttest/TimeoutTestClient/src/modelincludes.jl b/test/client/timeouttest/TimeoutTestClient/src/modelincludes.jl deleted file mode 100644 index af7c2e0..0000000 --- a/test/client/timeouttest/TimeoutTestClient/src/modelincludes.jl +++ /dev/null @@ -1,4 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -include("models/model_DelayresponseGet200Response.jl") diff --git a/test/client/timeouttest/TimeoutTestClient/src/models/model_DelayresponseGet200Response.jl b/test/client/timeouttest/TimeoutTestClient/src/models/model_DelayresponseGet200Response.jl deleted file mode 100644 index caa9121..0000000 --- a/test/client/timeouttest/TimeoutTestClient/src/models/model_DelayresponseGet200Response.jl +++ /dev/null @@ -1,36 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""_delayresponse_get_200_response - - DelayresponseGet200Response(; - delay_seconds=nothing, - ) - - - delay_seconds::String -""" -Base.@kwdef mutable struct DelayresponseGet200Response <: OpenAPI.APIModel - delay_seconds::Union{Nothing, String} = nothing - - function DelayresponseGet200Response(delay_seconds, ) - o = new(delay_seconds, ) - OpenAPI.validate_properties(o) - return o - end -end # type DelayresponseGet200Response - -const _property_types_DelayresponseGet200Response = Dict{Symbol,String}(Symbol("delay_seconds")=>"String", ) -OpenAPI.property_type(::Type{ DelayresponseGet200Response }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_DelayresponseGet200Response[name]))} - -function OpenAPI.check_required(o::DelayresponseGet200Response) - true -end - -function OpenAPI.validate_properties(o::DelayresponseGet200Response) - OpenAPI.validate_property(DelayresponseGet200Response, Symbol("delay_seconds"), o.delay_seconds) -end - -function OpenAPI.validate_property(::Type{ DelayresponseGet200Response }, name::Symbol, val) - -end diff --git a/test/client/timeouttest/generate.sh b/test/client/timeouttest/generate.sh deleted file mode 100755 index 7291031..0000000 --- a/test/client/timeouttest/generate.sh +++ /dev/null @@ -1,5 +0,0 @@ -java -jar openapi-generator-cli.jar generate \ - -i ../../specs/timeouttest.yaml \ - -g julia-client \ - -o TimeoutTestClient \ - --additional-properties=packageName=TimeoutTestClient diff --git a/test/client/timeouttest/runtests.jl b/test/client/timeouttest/runtests.jl deleted file mode 100644 index 4d4a4c2..0000000 --- a/test/client/timeouttest/runtests.jl +++ /dev/null @@ -1,78 +0,0 @@ -module TimeoutTests - -include(joinpath(@__DIR__, "TimeoutTestClient", "src", "TimeoutTestClient.jl")) -using .TimeoutTestClient -using Test -using JSON -using HTTP -using OpenAPI -using OpenAPI.Clients -import OpenAPI.Clients: Client, with_timeout, ApiException - -const M = TimeoutTestClient -const server = "http://127.0.0.1:8081" - -function test_normal_operation(client, delay_secs) - @info("timeout default, delay $delay_secs secs") - api = M.DefaultApi(client) - api_return, http_resp = delayresponse_get(api, delay_secs) - @test http_resp.status == 200 - @test api_return.delay_seconds == string(delay_secs) -end - -function test_timeout_operation(client, timeout_secs, delay_secs) - @info("timeout $timeout_secs secs, delay $delay_secs secs") - with_timeout(client, timeout_secs) do client - try - api = M.DefaultApi(client) - delayresponse_get(api, delay_secs) - error("Timeout not thrown") - catch ex - @test isa(ex, ApiException) - @test ex.status == 0 - @test startswith(ex.reason, "Operation timed out") - end - end -end - -function test_longpoll_timeout_operation(client, timeout_secs, delay_secs) - @info("timeout $timeout_secs secs, delay $delay_secs secs") - with_timeout(client, timeout_secs) do client - try - channel = Channel{Any}(10) - api = M.DefaultApi(client) - api_return, http_resp = longpollstream_get(api, channel, delay_secs) - take!(channel) - error("Timeout not thrown") - catch ex - @test OpenAPI.Clients.is_longpoll_timeout(ex) - end - end -end - -function runtests(httplib::Symbol) - @testset "timeout_tests ($httplib backend)" begin - @info("TimeoutTest ($httplib backend)") - client = Client(server; httplib=httplib) - - test_normal_operation(client, 10) - - for timeout_secs in (5, 120) # test different timeouts - delay_secs = timeout_secs + 60 - test_timeout_operation(client, timeout_secs, delay_secs) - - # but the client should still be usable - test_normal_operation(client, 10) - end - - # also test a long delay in general (default libcurl timeout is 0) - test_normal_operation(client, 160) - end - @testset "longpoll_timeout_tests ($httplib backend)" begin - @info("TimeoutTest ($httplib backend)") - client = Client(server; httplib=httplib) - test_longpoll_timeout_operation(client, 20, 60) - end -end - -end # module TimeoutTests diff --git a/test/client/utilstests.jl b/test/client/utilstests.jl deleted file mode 100644 index 3c065ae..0000000 --- a/test/client/utilstests.jl +++ /dev/null @@ -1,343 +0,0 @@ -using OpenAPI -using OpenAPI.Clients -using Test -using Dates -using TimeZones -using Base64 -using Downloads -using HTTP - -function test_date() - dt_now = now() - dt_string = string(ZonedDateTime(dt_now, localzone())) - dt = OpenAPI.str2zoneddatetime(convert(Vector{UInt8}, codeunits(dt_string))) - @test dt == OpenAPI.str2zoneddatetime(dt_now) - @test dt_string == string(dt) - - dt_string = string(dt_now) - dt = OpenAPI.str2datetime(convert(Vector{UInt8}, codeunits(dt_string))) - @test dt == OpenAPI.str2datetime(dt_now) - @test dt_string == string(dt) - - dt_string = string(Date(dt_now)) - dt = OpenAPI.str2date(convert(Vector{UInt8}, codeunits(dt_string))) - @test dt == OpenAPI.str2date(Date(dt_now)) - @test dt_string == string(dt) - - dates = ["2017-11-14", "2020-01-01"] - timesep = [" ", "T"] - times = - ["11:03:53", "11:03:53.12", "11:03:53.123", "11:03:53.123456", "11:03:53.123456789"] - timezones = ["", "+10:00", "-10:00", "Z"] - - for date in dates - d = OpenAPI.str2date(date) - for sep in timesep - for time in times - reduced_time = length(time) > 12 ? SubString(time, 1, 12) : time - t = Time(reduced_time) - for tz in timezones - dt_string = date * sep * time * tz - reduced_dt_string = date * sep * reduced_time * tz - @test OpenAPI.reduce_to_ms_precision(dt_string) == reduced_dt_string - zdt = OpenAPI.str2zoneddatetime(convert(Vector{UInt8}, codeunits(dt_string))) - dt = OpenAPI.str2datetime(convert(Vector{UInt8}, codeunits(dt_string))) - @test d == Date(zdt) - @test d == Date(dt) - @test t == Time(zdt) - @test t == Time(dt) - end - end - end - end - - for tz in timezones - @test OpenAPI.str2date("2017-11-14"*tz) == Date(2017, 11, 14) - end -end - -function as_taskfailedexception(ex) - try - task = @async throw(ex) - wait(task) - catch ex - return ex - end -end - -function test_longpoll_exception_check() - resp = OpenAPI.Clients.Downloads.Response("http", "http://localhost", 200, "no error", []) - - # HTTP.jl 1.x and 2.0 have different `TimeoutError`/`ConnectError` constructors. - mk_timeout_err() = OpenAPI.Clients._HTTP_V2 ? HTTP.TimeoutError("read_idle", 20_000_000) : HTTP.TimeoutError(20) - mk_connect_err() = HTTP.ConnectError("http://localhost", ErrorException("dns error")) - - not_longpoll_timeouts = [ - OpenAPI.Clients.Downloads.RequestError("http://localhost", 500, "not timeout error", resp), - OpenAPI.Clients.HTTPRequestError(mk_timeout_err(), 20, nothing), - OpenAPI.Clients.HTTPRequestError(mk_timeout_err(), nothing), - OpenAPI.Clients.HTTPRequestError(mk_connect_err()), - ] - - longpoll_timeouts = [ - OpenAPI.Clients.Downloads.RequestError("http://localhost", 200, "Operation timed out after 300 milliseconds with 0 bytes received", resp), # timeout error - OpenAPI.Clients.HTTPRequestError(mk_timeout_err(), 20, HTTP.Response(200, "hello")), - ] - - @test OpenAPI.Clients.is_longpoll_timeout("not an exception") == false - - for reqerr in not_longpoll_timeouts - openapiex = OpenAPI.Clients.ApiException(reqerr) - @test OpenAPI.Clients.is_longpoll_timeout(openapiex) == false - @test OpenAPI.Clients.is_longpoll_timeout(as_taskfailedexception(openapiex)) == false - end - - for reqerr in longpoll_timeouts - openapiex = OpenAPI.Clients.ApiException(reqerr) - @test OpenAPI.Clients.is_longpoll_timeout(openapiex) - @test OpenAPI.Clients.is_longpoll_timeout(as_taskfailedexception(openapiex)) - end - - notlp = OpenAPI.Clients.ApiException(first(not_longpoll_timeouts)) - lp = OpenAPI.Clients.ApiException(first(longpoll_timeouts)) - @test OpenAPI.Clients.is_longpoll_timeout(CompositeException([notlp, lp])) - @test OpenAPI.Clients.is_longpoll_timeout(CompositeException([notlp, as_taskfailedexception(lp)])) - @test OpenAPI.Clients.is_longpoll_timeout(CompositeException([notlp, as_taskfailedexception(notlp)])) == false -end - -function test_request_interrupted_exception_check() - ex1 = OpenAPI.InvocationException("request was interrupted") - ex2 = ArgumentError("request interrupted") - ex3 = OpenAPI.InvocationException("not request interrupted") - - @test OpenAPI.Clients.is_request_interrupted(ex1) - @test !OpenAPI.Clients.is_request_interrupted(ex2) - @test !OpenAPI.Clients.is_request_interrupted(ex3) - - @test OpenAPI.Clients.is_request_interrupted(as_taskfailedexception(ex1)) - @test !OpenAPI.Clients.is_request_interrupted(as_taskfailedexception(ex2)) - @test !OpenAPI.Clients.is_request_interrupted(as_taskfailedexception(ex3)) - - @test OpenAPI.Clients.is_request_interrupted(CompositeException([ex1, ex2])) - @test !OpenAPI.Clients.is_request_interrupted(CompositeException([ex2, ex3])) -end - -function OpenAPI.val_format(val::AbstractString, ::Val{:testformat}) - return val == "testvalue" -end -function OpenAPI.val_format(val::Integer, ::Val{:testformat}) - return val == 111 -end -function OpenAPI.val_format(val::AbstractFloat, ::Val{:testformat}) - return val == 111.111 -end - -function test_custom_format_validations() - @test OpenAPI.val_format("testvalue", "testformat") - @test !OpenAPI.val_format("invalidvalue", "testformat") - @test OpenAPI.val_format("anyvalue", "unknownformat") - - @test OpenAPI.val_format(111, "testformat") - @test !OpenAPI.val_format(222, "testformat") - @test OpenAPI.val_format(111, "unknownformat") - - @test OpenAPI.val_format(111.111, "testformat") - @test !OpenAPI.val_format(222.222, "testformat") - @test OpenAPI.val_format(111.111, "unknownformat") - - return nothing -end - -function test_format_validations() - @test OpenAPI.val_format(typemax(Float32), "float") - @test OpenAPI.val_format(typemax(Float64), "double") - @test OpenAPI.val_multiple_of(10.0, 5.0) - @test !OpenAPI.val_multiple_of(10.0, 3.0) - - b64str = String(base64encode("test string")) - @test OpenAPI.val_format(b64str, "byte") - @test !OpenAPI.val_format("not base64", "byte") -end - -function test_validations() - # maximum - @test_throws OpenAPI.ValidationException OpenAPI.validate_param("test_param", "test_model", :maximum, 11, 10, true) - @test_throws OpenAPI.ValidationException OpenAPI.validate_param("test_param", "test_model", :maximum, 11, 10, false) - @test_throws OpenAPI.ValidationException OpenAPI.validate_param("test_param", "test_model", :maximum, 10, 10, true) - @test OpenAPI.validate_param("test_param", "test_model", :maximum, 10, 10, false) === nothing - @test OpenAPI.validate_param("test_param", "test_model", :maximum, 1, 10, false) === nothing - - # minimum - @test_throws OpenAPI.ValidationException OpenAPI.validate_param("test_param", "test_model", :minimum, 10, 11, true) - @test_throws OpenAPI.ValidationException OpenAPI.validate_param("test_param", "test_model", :minimum, 10, 11, false) - @test_throws OpenAPI.ValidationException OpenAPI.validate_param("test_param", "test_model", :minimum, 10, 10, true) - @test OpenAPI.validate_param("test_param", "test_model", :minimum, 10, 10, false) === nothing - @test OpenAPI.validate_param("test_param", "test_model", :minimum, 10, 1, false) === nothing - - # maxLength, maxItems, maxProperties - for test in (:maxLength, :maxItems, :maxProperties) - for items in (1:10, Dict(zip(1:10, 1:10)), [1:10...]) - @test OpenAPI.validate_param("test_param", "test_model", test, items, 10) === nothing - end - for items in (1:2, Dict(zip(1:2, 1:2)), [1:2...]) - @test OpenAPI.validate_param("test_param", "test_model", test, items, 10) === nothing - end - end - - # minLength, minItems, minProperties - for test in (:minLength, :minItems, :minProperties) - for items in (1:10, Dict(zip(1:10, 1:10)), [1:10...]) - @test OpenAPI.validate_param("test_param", "test_model", test, items, 10) === nothing - @test OpenAPI.validate_param("test_param", "test_model", test, items, 1) === nothing - end - end - - # unique - @test OpenAPI.validate_param("test_param", "test_model", :uniqueItems, [1, 2, 3], true) === nothing - @test OpenAPI.validate_param("test_param", "test_model", :uniqueItems, [1, 2, 2], false) === nothing - @test_throws OpenAPI.ValidationException OpenAPI.validate_param("test_param", "test_model", :uniqueItems, [1, 2, 2], true) - - # pattern - @test OpenAPI.validate_param("test_param", "test_model", :pattern, "test", r"[a-z]+") === nothing - @test_throws OpenAPI.ValidationException OpenAPI.validate_param("test_param", "test_model", :pattern, "test", r"[0-9]+") - - # enum - @test OpenAPI.validate_param("test_param", "test_model", :enum, [:a, :b, :b], [:a, :b, :c]) === nothing - @test_throws OpenAPI.ValidationException OpenAPI.validate_param("test_param", "test_model", :enum, [:a, :b, :c, :d], [:a, :b, :c]) - - # custom format Validations - test_format_validations() - test_custom_format_validations() - - return nothing -end - -struct TestHasPropertyInner <: OpenAPI.APIModel - testval::Union{Nothing,String} - - function TestHasPropertyInner(; testval=nothing) - return new(testval) - end -end - -struct TestHasProperty <: OpenAPI.APIModel - inner::Union{Nothing,TestHasPropertyInner} - - function TestHasProperty(; inner=nothing) - return new(inner) - end -end - -function test_has_property() - teststruct = TestHasProperty() - - @test !OpenAPI.Clients.haspropertyat(teststruct, :inner, :testval) - @test !OpenAPI.Clients.haspropertyat(teststruct, "inner", "testval") - @test !OpenAPI.Clients.haspropertyat(teststruct, :inner) - - teststruct = TestHasProperty(; inner=TestHasPropertyInner()) - @test !OpenAPI.Clients.haspropertyat(teststruct, :inner, :testval) - @test !OpenAPI.Clients.haspropertyat(teststruct, "inner", "testval") - @test OpenAPI.Clients.haspropertyat(teststruct, :inner) - - teststruct = TestHasProperty(; inner=TestHasPropertyInner(; testval="test")) - @test OpenAPI.Clients.haspropertyat(teststruct, :inner, :testval) - @test OpenAPI.Clients.haspropertyat(teststruct, "inner", "testval") - @test OpenAPI.Clients.haspropertyat(teststruct, :inner) - @test OpenAPI.Clients.getpropertyat(teststruct, :inner, :testval) == "test" -end - - -struct InvalidModel <: OpenAPI.APIModel - test::Any - - function InvalidModel(; test=nothing) - return new(test) - end -end - -function test_misc() - @test isa(OpenAPI.OpenAPIException("test"), Exception) - @test_throws Exception OpenAPI.property_type(InvalidModel(), :test) - - json = Dict{String,Any}() - @test OpenAPI.from_json(Any, json) === json - @test OpenAPI.from_json(String, json) == "{}" - @test isa(OpenAPI.from_json(Dict{Any,Any}, json), Dict{Any,Any}) -end - -const content_disposition_tests = [ - (content_disposition="attachment; filename=content.txt", content_type="", filename="content.txt"), - (content_disposition="attachment; filename*=UTF-8''filename.txt", content_type="", filename="filename.txt"), - (content_disposition="attachment; filename=\"Image File\"; filename*=utf-8''UTF8ImageFile", content_type="", filename="Image File"), - (content_disposition="", content_type="", filename="response"), - (content_disposition="", content_type="image/jpg", filename="response"), -] - -const non_ascii_content_disposition_tests = [ - (content_disposition="attachment; filename=\"चित्त.jpg\"", content_type="", filename="चित्त.jpg"), -] - -function test_storefile() - # TODO: Checks for HTTP.jl backend - for test_data in content_disposition_tests - headers = [ - "Content-Disposition" => test_data.content_disposition, - "Content-Type" => test_data.content_type, - ] - responses = [ - Downloads.Response("GET", "http://test/", 200, "", headers), - HTTP.Response(200, headers, "") - ] - - for resp in responses - @test OpenAPI.Clients.extract_filename(resp) == test_data.filename - end - end - - for test_data in non_ascii_content_disposition_tests - headers = [ - "Content-Disposition" => test_data.content_disposition, - "Content-Type" => test_data.content_type, - ] - resp = Downloads.Response("GET", "http://test/", 200, "", headers) - @test OpenAPI.Clients.extract_filename(resp) == test_data.filename - end - - mktempdir() do tmpdir - test_data = content_disposition_tests[1] - file_contents = "test file data" - - headers = [ - "Content-Disposition" => test_data.content_disposition, - "Content-Type" => test_data.content_type, - ] - - responses = [ - OpenAPI.Clients.ApiResponse(Downloads.Response("GET", "http://test/", 200, "", headers)), - ] - - for resp in responses - - # test extraction of filename from headers - result, http_response, filepath = OpenAPI.Clients.storefile(; folder=tmpdir) do - return file_contents, resp - end - - @test result == file_contents - @test http_response == resp - @test filepath == joinpath(tmpdir, test_data.filename) - @test read(filepath, String) == file_contents - - # test overriding filename - result, http_response, filepath = OpenAPI.Clients.storefile(; folder=tmpdir, filename="overridename.txt") do - return file_contents, resp - end - - @test result == file_contents - @test http_response == resp - @test filepath == joinpath(tmpdir, "overridename.txt") - @test read(filepath, String) == file_contents - end - end -end \ No newline at end of file diff --git a/test/corpus.jl b/test/corpus.jl new file mode 100644 index 0000000..4f04d59 --- /dev/null +++ b/test/corpus.jl @@ -0,0 +1,131 @@ +import Downloads +import SHA + +const OPENAPI_CORPUS_CASES = ( + ( + name = "Petstore", + url = "https://raw.githubusercontent.com/swagger-api/swagger-petstore/8f0dd286987880b4af7bce552aca3813166f3049/src/main/resources/openapi.yaml", + sha256 = "0d810997f6409d5cff6f0cf2c1466814ba52250a784cd841cacb93514c7a8502", + strict = true, + operations = 19, + schemas = 6, + models = 10, + warning_codes = Set{Symbol}(), + large = false, + ), + ( + name = "Discord", + url = "https://raw.githubusercontent.com/discord/discord-api-spec/74fda0fad044407ea280043a03ffc9bc64c4e49e/specs/openapi.json", + sha256 = "8c1d0707ccdf8e380a86dfba04058820a66435c1b69d5ed82e04dd8c0520dd73", + strict = true, + operations = 242, + schemas = 539, + models = 952, + warning_codes = Set{Symbol}(), + large = false, + ), + ( + name = "Stripe", + url = "https://raw.githubusercontent.com/stripe/openapi/8da624f9b4f65178eb2e2c2b6fc80162a6c0dceb/latest/openapi.spec3.json", + sha256 = "6f3623aece40493eec2f5e3e631219f8c6bffa4f477e3807a4bf785ad377f237", + strict = false, + operations = 621, + schemas = 1596, + models = 13406, + warning_codes = Set([ + :invalid_deep_object_schema, + :legacy_nullable_without_type, + ]), + large = true, + ), + ( + name = "GitHub", + url = "https://raw.githubusercontent.com/github/rest-api-description/5e28810649ba41b5483753ba74f976f83856a504/descriptions/api.github.com/api.github.com.json", + sha256 = "04a2597b999c6d13d5269334d0c105252b8b58321c9fcdade40ddc50302220fb", + strict = false, + operations = 1216, + schemas = 967, + models = 7125, + warning_codes = Set([ + :ambiguous_path_template, + :ignored_content_type_header, + :legacy_nullable_without_type, + ]), + large = true, + ), +) + +function run_corpus_case(case) + started = time() + directory = mktempdir() + path = joinpath(directory, lowercase(case.name) * ".openapi") + Downloads.download(case.url, path) + @test bytes2hex(SHA.sha256(read(path))) == case.sha256 + downloaded = time() + + # Use the public resource ceilings. This keeps the corpus test honest for + # callers that point OpenAPI.jl at these documents without tuning options. + api = OpenAPI.normalize(path; strict = case.strict) + @test length(api.operations) == case.operations + @test length(api.schemas) == case.schemas + normalized = time() + + plan = OpenAPI.plan(api; name = case.name * "CorpusClient", strict = case.strict) + @test length(plan.operations) == case.operations + @test length(plan.models) == case.models + @test Set( + diagnostic.code for diagnostic in plan.diagnostics if + diagnostic.severity === :warning + ) == case.warning_codes + planned = time() + + source = OpenAPI.client(plan) + generated_at = time() + parsed = Meta.parseall(source; filename = case.name * "CorpusClient.jl") + @test parsed isa Expr + parsed = nothing + case.large && GC.gc(false) + host = Module(Symbol(case.name, :CorpusHost)) + Base.include_string(host, source, case.name * "CorpusClient.jl") + generated = Base.invokelatest( + getfield, + host, + Symbol(case.name, :CorpusClient), + ) + operation = Base.invokelatest( + getfield, + generated, + Symbol(first(plan.operations).name), + ) + @test operation isa Function + finished = time() + @info "OpenAPI corpus case compiled" name = case.name download_seconds = + round(downloaded - started; digits = 2) normalize_seconds = + round(normalized - downloaded; digits = 2) plan_seconds = + round(planned - normalized; digits = 2) generate_seconds = + round(generated_at - planned; digits = 2) compile_seconds = + round(finished - generated_at; digits = 2) source_megabytes = + round(sizeof(source) / 1024^2; digits = 2) + return nothing +end + +@testset "pinned real-world OpenAPI corpus" begin + mode = lowercase(get(ENV, "OPENAPI_CORPUS_TESTS", "small")) + mode in ("small", "all") || + throw(ArgumentError("OPENAPI_CORPUS_TESTS must be `small` or `all`")) + selected = lowercase(strip(get(ENV, "OPENAPI_CORPUS_CASE", ""))) + names = Set(lowercase(case.name) for case in OPENAPI_CORPUS_CASES) + isempty(selected) || selected in names || throw( + ArgumentError( + "OPENAPI_CORPUS_CASE must name one of " * + join(sort!(collect(names)), ", "), + ), + ) + for case in OPENAPI_CORPUS_CASES + isempty(selected) || lowercase(case.name) == selected || continue + isempty(selected) && mode == "small" && case.large && continue + @testset "$(case.name)" begin + run_corpus_case(case) + end + end +end diff --git a/test/deep_object/DeepClient/.openapi-generator-ignore b/test/deep_object/DeepClient/.openapi-generator-ignore deleted file mode 100644 index 7484ee5..0000000 --- a/test/deep_object/DeepClient/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/test/deep_object/DeepClient/.openapi-generator/FILES b/test/deep_object/DeepClient/.openapi-generator/FILES deleted file mode 100644 index e0018f3..0000000 --- a/test/deep_object/DeepClient/.openapi-generator/FILES +++ /dev/null @@ -1,9 +0,0 @@ -README.md -docs/FindPetsByStatus200Response.md -docs/FindPetsByStatusStatusParameter.md -docs/PetApi.md -src/DeepClient.jl -src/apis/api_PetApi.jl -src/modelincludes.jl -src/models/model_FindPetsByStatus200Response.jl -src/models/model_FindPetsByStatusStatusParameter.jl diff --git a/test/deep_object/DeepClient/.openapi-generator/VERSION b/test/deep_object/DeepClient/.openapi-generator/VERSION deleted file mode 100644 index 757e674..0000000 --- a/test/deep_object/DeepClient/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.0.0-SNAPSHOT \ No newline at end of file diff --git a/test/deep_object/DeepClient/Project.toml b/test/deep_object/DeepClient/Project.toml deleted file mode 100644 index 6925b14..0000000 --- a/test/deep_object/DeepClient/Project.toml +++ /dev/null @@ -1,17 +0,0 @@ -name = "DeepClient" -uuid = "a6d6279c-b61a-4cd3-8410-d9d61a86b071" -authors = ["vdayanand "] -version = "0.1.0" - -[deps] -Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" -HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" -OpenAPI = "d5e62ea6-ddf3-4d43-8e4c-ad5e6c8bfd7d" -TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" -URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" - -[extras] -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[targets] -test = ["Test"] diff --git a/test/deep_object/DeepClient/README.md b/test/deep_object/DeepClient/README.md deleted file mode 100644 index 0c1c45a..0000000 --- a/test/deep_object/DeepClient/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# Julia API client for DeepClient - -This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. - -## Overview -This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. - -- API version: 1.0.0 -- Build package: org.openapitools.codegen.languages.JuliaClientCodegen - - -## Installation -Place the Julia files generated under the `src` folder in your Julia project. Include DeepClient.jl in the project code. -It would include the module named DeepClient. - -Documentation is generated as markdown files under the `docs` folder. You can include them in your project documentation. -Documentation is also embedded in Julia which can be used with a Julia specific documentation generator. - -## API Endpoints - -Class | Method ------------- | ------------- -*PetApi* | [**find_pets_by_status**](docs/PetApi.md#find_pets_by_status)
**GET** /pet/findByStatus
Finds Pets by status - - -## Models - - - [FindPetsByStatus200Response](docs/FindPetsByStatus200Response.md) - - [FindPetsByStatusStatusParameter](docs/FindPetsByStatusStatusParameter.md) - - - -## Authorization -Endpoints do not require authorization. - - -## Author - - - diff --git a/test/deep_object/DeepClient/docs/FindPetsByStatus200Response.md b/test/deep_object/DeepClient/docs/FindPetsByStatus200Response.md deleted file mode 100644 index 7747550..0000000 --- a/test/deep_object/DeepClient/docs/FindPetsByStatus200Response.md +++ /dev/null @@ -1,12 +0,0 @@ -# FindPetsByStatus200Response - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**result** | [***FindPetsByStatusStatusParameter**](FindPetsByStatusStatusParameter.md) | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/deep_object/DeepClient/docs/FindPetsByStatus200ResponseResult.md b/test/deep_object/DeepClient/docs/FindPetsByStatus200ResponseResult.md deleted file mode 100644 index b388b95..0000000 --- a/test/deep_object/DeepClient/docs/FindPetsByStatus200ResponseResult.md +++ /dev/null @@ -1,13 +0,0 @@ -# FindPetsByStatus200ResponseResult - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**name** | **String** | | [optional] [default to nothing] -**statuses** | [**Vector{FindPetsByStatusStatusParameterStatusesInnerInner}**](FindPetsByStatusStatusParameterStatusesInnerInner.md) | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/deep_object/DeepClient/docs/FindPetsByStatusStatusParameter.md b/test/deep_object/DeepClient/docs/FindPetsByStatusStatusParameter.md deleted file mode 100644 index 343827b..0000000 --- a/test/deep_object/DeepClient/docs/FindPetsByStatusStatusParameter.md +++ /dev/null @@ -1,13 +0,0 @@ -# FindPetsByStatusStatusParameter - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**name** | **String** | | [optional] [default to nothing] -**statuses** | **Vector{String}** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/deep_object/DeepClient/docs/FindPetsByStatusStatusParameterStatusesInner.md b/test/deep_object/DeepClient/docs/FindPetsByStatusStatusParameterStatusesInner.md deleted file mode 100644 index f435999..0000000 --- a/test/deep_object/DeepClient/docs/FindPetsByStatusStatusParameterStatusesInner.md +++ /dev/null @@ -1,12 +0,0 @@ -# FindPetsByStatusStatusParameterStatusesInner - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**type** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/deep_object/DeepClient/docs/FindPetsByStatusStatusParameterStatusesInnerInner.md b/test/deep_object/DeepClient/docs/FindPetsByStatusStatusParameterStatusesInnerInner.md deleted file mode 100644 index 4229b9a..0000000 --- a/test/deep_object/DeepClient/docs/FindPetsByStatusStatusParameterStatusesInnerInner.md +++ /dev/null @@ -1,12 +0,0 @@ -# FindPetsByStatusStatusParameterStatusesInnerInner - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**type** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/deep_object/DeepClient/docs/PetApi.md b/test/deep_object/DeepClient/docs/PetApi.md deleted file mode 100644 index 05dee55..0000000 --- a/test/deep_object/DeepClient/docs/PetApi.md +++ /dev/null @@ -1,39 +0,0 @@ -# PetApi - -All URIs are relative to *http://petstore.swagger.io/v2* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**find_pets_by_status**](PetApi.md#find_pets_by_status) | **GET** /pet/findByStatus | Finds Pets by status - - -# **find_pets_by_status** -> find_pets_by_status(_api::PetApi, status::FindPetsByStatusStatusParameter; _mediaType=nothing) -> FindPetsByStatus200Response, OpenAPI.Clients.ApiResponse
-> find_pets_by_status(_api::PetApi, response_stream::Channel, status::FindPetsByStatusStatusParameter; _mediaType=nothing) -> Channel{ FindPetsByStatus200Response }, OpenAPI.Clients.ApiResponse - -Finds Pets by status - -Multiple status values can be provided with comma separated strings - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **PetApi** | API context | -**status** | [**FindPetsByStatusStatusParameter**](.md)| Status values that need to be considered for filter | [default to nothing] - -### Return type - -[**FindPetsByStatus200Response**](FindPetsByStatus200Response.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - diff --git a/test/deep_object/DeepClient/src/DeepClient.jl b/test/deep_object/DeepClient/src/DeepClient.jl deleted file mode 100644 index df7bca5..0000000 --- a/test/deep_object/DeepClient/src/DeepClient.jl +++ /dev/null @@ -1,16 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -module DeepClient - -using Dates, TimeZones -using OpenAPI -using OpenAPI.Clients - -const API_VERSION = "1.0.0" - -include("modelincludes.jl") - -include("apis/api_PetApi.jl") - -end # module DeepClient diff --git a/test/deep_object/DeepClient/src/apis/api_PetApi.jl b/test/deep_object/DeepClient/src/apis/api_PetApi.jl deleted file mode 100644 index cde7e6a..0000000 --- a/test/deep_object/DeepClient/src/apis/api_PetApi.jl +++ /dev/null @@ -1,46 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -struct PetApi <: OpenAPI.APIClientImpl - client::OpenAPI.Clients.Client -end - -""" -The default API base path for APIs in `PetApi`. -This can be used to construct the `OpenAPI.Clients.Client` instance. -""" -basepath(::Type{ PetApi }) = "http://petstore.swagger.io/v2" - -const _returntypes_find_pets_by_status_PetApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => FindPetsByStatus200Response, - Regex("^" * replace("400", "x"=>".") * "\$") => Nothing, -) - -function _oacinternal_find_pets_by_status(_api::PetApi, status::FindPetsByStatusStatusParameter; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_find_pets_by_status_PetApi, "/pet/findByStatus", []) - OpenAPI.Clients.set_param(_ctx.query, "status", status; style="deepObject", location=:query, is_explode=true) # type FindPetsByStatusStatusParameter - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Finds Pets by status - -Multiple status values can be provided with comma separated strings - -Params: -- status::FindPetsByStatusStatusParameter (required) - -Return: FindPetsByStatus200Response, OpenAPI.Clients.ApiResponse -""" -function find_pets_by_status(_api::PetApi, status::FindPetsByStatusStatusParameter; _mediaType=nothing) - _ctx = _oacinternal_find_pets_by_status(_api, status; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function find_pets_by_status(_api::PetApi, response_stream::Channel, status::FindPetsByStatusStatusParameter; _mediaType=nothing) - _ctx = _oacinternal_find_pets_by_status(_api, status; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -export find_pets_by_status diff --git a/test/deep_object/DeepClient/src/deep.jl b/test/deep_object/DeepClient/src/deep.jl deleted file mode 100644 index 2088dc6..0000000 --- a/test/deep_object/DeepClient/src/deep.jl +++ /dev/null @@ -1,16 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -module deep - -using Dates, TimeZones -using OpenAPI -using OpenAPI.Clients - -const API_VERSION = "1.0.0" - -include("modelincludes.jl") - -include("apis/api_PetApi.jl") - -end # module deep diff --git a/test/deep_object/DeepClient/src/modelincludes.jl b/test/deep_object/DeepClient/src/modelincludes.jl deleted file mode 100644 index 74221b3..0000000 --- a/test/deep_object/DeepClient/src/modelincludes.jl +++ /dev/null @@ -1,5 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -include("models/model_FindPetsByStatus200Response.jl") -include("models/model_FindPetsByStatusStatusParameter.jl") diff --git a/test/deep_object/DeepClient/src/models/model_FindPetsByStatus200Response.jl b/test/deep_object/DeepClient/src/models/model_FindPetsByStatus200Response.jl deleted file mode 100644 index 76efe04..0000000 --- a/test/deep_object/DeepClient/src/models/model_FindPetsByStatus200Response.jl +++ /dev/null @@ -1,30 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""findPetsByStatus_200_response - - FindPetsByStatus200Response(; - result=nothing, - ) - - - result::FindPetsByStatusStatusParameter -""" -Base.@kwdef mutable struct FindPetsByStatus200Response <: OpenAPI.APIModel - result = nothing # spec type: Union{ Nothing, FindPetsByStatusStatusParameter } - - function FindPetsByStatus200Response(result, ) - OpenAPI.validate_property(FindPetsByStatus200Response, Symbol("result"), result) - return new(result, ) - end -end # type FindPetsByStatus200Response - -const _property_types_FindPetsByStatus200Response = Dict{Symbol,String}(Symbol("result")=>"FindPetsByStatusStatusParameter", ) -OpenAPI.property_type(::Type{ FindPetsByStatus200Response }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_FindPetsByStatus200Response[name]))} - -function check_required(o::FindPetsByStatus200Response) - true -end - -function OpenAPI.validate_property(::Type{ FindPetsByStatus200Response }, name::Symbol, val) -end diff --git a/test/deep_object/DeepClient/src/models/model_FindPetsByStatus200ResponseResult.jl b/test/deep_object/DeepClient/src/models/model_FindPetsByStatus200ResponseResult.jl deleted file mode 100644 index 325cad5..0000000 --- a/test/deep_object/DeepClient/src/models/model_FindPetsByStatus200ResponseResult.jl +++ /dev/null @@ -1,34 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""findPetsByStatus_200_response_result - - FindPetsByStatus200ResponseResult(; - name=nothing, - statuses=nothing, - ) - - - name::String - - statuses::Vector{FindPetsByStatusStatusParameterStatusesInnerInner} -""" -Base.@kwdef mutable struct FindPetsByStatus200ResponseResult <: OpenAPI.APIModel - name::Union{Nothing, String} = nothing - statuses::Union{Nothing, Vector} = nothing # spec type: Union{ Nothing, Vector{FindPetsByStatusStatusParameterStatusesInnerInner} } - - function FindPetsByStatus200ResponseResult(name, statuses, ) - OpenAPI.validate_property(FindPetsByStatus200ResponseResult, Symbol("name"), name) - OpenAPI.validate_property(FindPetsByStatus200ResponseResult, Symbol("statuses"), statuses) - return new(name, statuses, ) - end -end # type FindPetsByStatus200ResponseResult - -const _property_types_FindPetsByStatus200ResponseResult = Dict{Symbol,String}(Symbol("name")=>"String", Symbol("statuses")=>"Vector{FindPetsByStatusStatusParameterStatusesInnerInner}", ) -OpenAPI.property_type(::Type{ FindPetsByStatus200ResponseResult }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_FindPetsByStatus200ResponseResult[name]))} - -function check_required(o::FindPetsByStatus200ResponseResult) - true -end - -function OpenAPI.validate_property(::Type{ FindPetsByStatus200ResponseResult }, name::Symbol, val) -end diff --git a/test/deep_object/DeepClient/src/models/model_FindPetsByStatusStatusParameter.jl b/test/deep_object/DeepClient/src/models/model_FindPetsByStatusStatusParameter.jl deleted file mode 100644 index 15507e3..0000000 --- a/test/deep_object/DeepClient/src/models/model_FindPetsByStatusStatusParameter.jl +++ /dev/null @@ -1,34 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""findPetsByStatus_status_parameter - - FindPetsByStatusStatusParameter(; - name=nothing, - statuses=nothing, - ) - - - name::String - - statuses::Vector{String} -""" -Base.@kwdef mutable struct FindPetsByStatusStatusParameter <: OpenAPI.APIModel - name::Union{Nothing, String} = nothing - statuses::Union{Nothing, Vector{String}} = nothing - - function FindPetsByStatusStatusParameter(name, statuses, ) - OpenAPI.validate_property(FindPetsByStatusStatusParameter, Symbol("name"), name) - OpenAPI.validate_property(FindPetsByStatusStatusParameter, Symbol("statuses"), statuses) - return new(name, statuses, ) - end -end # type FindPetsByStatusStatusParameter - -const _property_types_FindPetsByStatusStatusParameter = Dict{Symbol,String}(Symbol("name")=>"String", Symbol("statuses")=>"Vector{String}", ) -OpenAPI.property_type(::Type{ FindPetsByStatusStatusParameter }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_FindPetsByStatusStatusParameter[name]))} - -function check_required(o::FindPetsByStatusStatusParameter) - true -end - -function OpenAPI.validate_property(::Type{ FindPetsByStatusStatusParameter }, name::Symbol, val) -end diff --git a/test/deep_object/DeepClient/src/models/model_FindPetsByStatusStatusParameterStatusesInner.jl b/test/deep_object/DeepClient/src/models/model_FindPetsByStatusStatusParameterStatusesInner.jl deleted file mode 100644 index b5eb6a5..0000000 --- a/test/deep_object/DeepClient/src/models/model_FindPetsByStatusStatusParameterStatusesInner.jl +++ /dev/null @@ -1,30 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""findPetsByStatus_status_parameter_statuses_inner - - FindPetsByStatusStatusParameterStatusesInner(; - type=nothing, - ) - - - type::String -""" -Base.@kwdef mutable struct FindPetsByStatusStatusParameterStatusesInner <: OpenAPI.APIModel - type::Union{Nothing, String} = nothing - - function FindPetsByStatusStatusParameterStatusesInner(type, ) - OpenAPI.validate_property(FindPetsByStatusStatusParameterStatusesInner, Symbol("type"), type) - return new(type, ) - end -end # type FindPetsByStatusStatusParameterStatusesInner - -const _property_types_FindPetsByStatusStatusParameterStatusesInner = Dict{Symbol,String}(Symbol("type")=>"String", ) -OpenAPI.property_type(::Type{ FindPetsByStatusStatusParameterStatusesInner }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_FindPetsByStatusStatusParameterStatusesInner[name]))} - -function check_required(o::FindPetsByStatusStatusParameterStatusesInner) - true -end - -function OpenAPI.validate_property(::Type{ FindPetsByStatusStatusParameterStatusesInner }, name::Symbol, val) -end diff --git a/test/deep_object/DeepClient/src/models/model_FindPetsByStatusStatusParameterStatusesInnerInner.jl b/test/deep_object/DeepClient/src/models/model_FindPetsByStatusStatusParameterStatusesInnerInner.jl deleted file mode 100644 index 4f716c2..0000000 --- a/test/deep_object/DeepClient/src/models/model_FindPetsByStatusStatusParameterStatusesInnerInner.jl +++ /dev/null @@ -1,30 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""findPetsByStatus_status_parameter_statuses_inner_inner - - FindPetsByStatusStatusParameterStatusesInnerInner(; - type=nothing, - ) - - - type::String -""" -Base.@kwdef mutable struct FindPetsByStatusStatusParameterStatusesInnerInner <: OpenAPI.APIModel - type::Union{Nothing, String} = nothing - - function FindPetsByStatusStatusParameterStatusesInnerInner(type, ) - OpenAPI.validate_property(FindPetsByStatusStatusParameterStatusesInnerInner, Symbol("type"), type) - return new(type, ) - end -end # type FindPetsByStatusStatusParameterStatusesInnerInner - -const _property_types_FindPetsByStatusStatusParameterStatusesInnerInner = Dict{Symbol,String}(Symbol("type")=>"String", ) -OpenAPI.property_type(::Type{ FindPetsByStatusStatusParameterStatusesInnerInner }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_FindPetsByStatusStatusParameterStatusesInnerInner[name]))} - -function check_required(o::FindPetsByStatusStatusParameterStatusesInnerInner) - true -end - -function OpenAPI.validate_property(::Type{ FindPetsByStatusStatusParameterStatusesInnerInner }, name::Symbol, val) -end diff --git a/test/deep_object/DeepServer/.openapi-generator-ignore b/test/deep_object/DeepServer/.openapi-generator-ignore deleted file mode 100644 index 7484ee5..0000000 --- a/test/deep_object/DeepServer/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/test/deep_object/DeepServer/.openapi-generator/FILES b/test/deep_object/DeepServer/.openapi-generator/FILES deleted file mode 100644 index a9ca22d..0000000 --- a/test/deep_object/DeepServer/.openapi-generator/FILES +++ /dev/null @@ -1,9 +0,0 @@ -README.md -docs/FindPetsByStatus200Response.md -docs/FindPetsByStatusStatusParameter.md -docs/PetApi.md -src/DeepServer.jl -src/apis/api_PetApi.jl -src/modelincludes.jl -src/models/model_FindPetsByStatus200Response.jl -src/models/model_FindPetsByStatusStatusParameter.jl diff --git a/test/deep_object/DeepServer/.openapi-generator/VERSION b/test/deep_object/DeepServer/.openapi-generator/VERSION deleted file mode 100644 index 757e674..0000000 --- a/test/deep_object/DeepServer/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.0.0-SNAPSHOT \ No newline at end of file diff --git a/test/deep_object/DeepServer/Project.toml b/test/deep_object/DeepServer/Project.toml deleted file mode 100644 index c58820a..0000000 --- a/test/deep_object/DeepServer/Project.toml +++ /dev/null @@ -1,17 +0,0 @@ -name = "DeepServer" -uuid = "c178418e-95dc-4e61-99ad-6838d361ce75" -authors = ["vdayanand "] -version = "0.1.0" - -[deps] -Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" -HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" -OpenAPI = "d5e62ea6-ddf3-4d43-8e4c-ad5e6c8bfd7d" -TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" -URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" - -[extras] -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[targets] -test = ["Test"] diff --git a/test/deep_object/DeepServer/README.md b/test/deep_object/DeepServer/README.md deleted file mode 100644 index 92bfb50..0000000 --- a/test/deep_object/DeepServer/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Julia API server for DeepServer - -This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. - -## Overview -This API server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. - -- API version: 1.0.0 -- Build package: org.openapitools.codegen.languages.JuliaServerCodegen - - -## Installation -Place the Julia files generated under the `src` folder in your Julia project. Include DeepServer.jl in the project code. -It would include the module named DeepServer. - -Implement the server methods as listed below. They are also documented with the DeepServer module. -Launch a HTTP server with a router that has all handlers registered. A `register` method is provided in DeepServer module for convenience. - -```julia -register( - router::HTTP.Router, # Router to register handlers in - impl; # Module that implements the server methods - path_prefix::String="", # Prefix to be applied to all paths - optional_middlewares... # Optional middlewares to be applied to all handlers -) -``` - -Optional middlewares can be one or more of: -- `init`: called before the request is processed -- `pre_validation`: called after the request is parsed but before validation -- `pre_invoke`: called after validation but before the handler is invoked -- `post_invoke`: called after the handler is invoked but before the response is sent - -The order in which middlewares are invoked are: -`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` - - -## API Endpoints - -The following server methods must be implemented: - -Class | Method | HTTP request | Description ------------- | ------------- | ------------- | ------------- -*PetApi* | [**find_pets_by_status**](docs/PetApi.md#find_pets_by_status) | **GET** /pet/findByStatus | Finds Pets by status - - - -## Models - - - [FindPetsByStatus200Response](docs/FindPetsByStatus200Response.md) - - [FindPetsByStatusStatusParameter](docs/FindPetsByStatusStatusParameter.md) - - - -## Author - - - diff --git a/test/deep_object/DeepServer/docs/FindPetsByStatus200Response.md b/test/deep_object/DeepServer/docs/FindPetsByStatus200Response.md deleted file mode 100644 index 7747550..0000000 --- a/test/deep_object/DeepServer/docs/FindPetsByStatus200Response.md +++ /dev/null @@ -1,12 +0,0 @@ -# FindPetsByStatus200Response - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**result** | [***FindPetsByStatusStatusParameter**](FindPetsByStatusStatusParameter.md) | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/deep_object/DeepServer/docs/FindPetsByStatus200ResponseResult.md b/test/deep_object/DeepServer/docs/FindPetsByStatus200ResponseResult.md deleted file mode 100644 index b388b95..0000000 --- a/test/deep_object/DeepServer/docs/FindPetsByStatus200ResponseResult.md +++ /dev/null @@ -1,13 +0,0 @@ -# FindPetsByStatus200ResponseResult - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**name** | **String** | | [optional] [default to nothing] -**statuses** | [**Vector{FindPetsByStatusStatusParameterStatusesInnerInner}**](FindPetsByStatusStatusParameterStatusesInnerInner.md) | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/deep_object/DeepServer/docs/FindPetsByStatusStatusParameter.md b/test/deep_object/DeepServer/docs/FindPetsByStatusStatusParameter.md deleted file mode 100644 index 343827b..0000000 --- a/test/deep_object/DeepServer/docs/FindPetsByStatusStatusParameter.md +++ /dev/null @@ -1,13 +0,0 @@ -# FindPetsByStatusStatusParameter - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**name** | **String** | | [optional] [default to nothing] -**statuses** | **Vector{String}** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/deep_object/DeepServer/docs/FindPetsByStatusStatusParameterStatusesInner.md b/test/deep_object/DeepServer/docs/FindPetsByStatusStatusParameterStatusesInner.md deleted file mode 100644 index f435999..0000000 --- a/test/deep_object/DeepServer/docs/FindPetsByStatusStatusParameterStatusesInner.md +++ /dev/null @@ -1,12 +0,0 @@ -# FindPetsByStatusStatusParameterStatusesInner - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**type** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/deep_object/DeepServer/docs/FindPetsByStatusStatusParameterStatusesInnerInner.md b/test/deep_object/DeepServer/docs/FindPetsByStatusStatusParameterStatusesInnerInner.md deleted file mode 100644 index 4229b9a..0000000 --- a/test/deep_object/DeepServer/docs/FindPetsByStatusStatusParameterStatusesInnerInner.md +++ /dev/null @@ -1,12 +0,0 @@ -# FindPetsByStatusStatusParameterStatusesInnerInner - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**type** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/deep_object/DeepServer/docs/PetApi.md b/test/deep_object/DeepServer/docs/PetApi.md deleted file mode 100644 index b4d46f1..0000000 --- a/test/deep_object/DeepServer/docs/PetApi.md +++ /dev/null @@ -1,38 +0,0 @@ -# PetApi - -All URIs are relative to *http://petstore.swagger.io/v2* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**find_pets_by_status**](PetApi.md#find_pets_by_status) | **GET** /pet/findByStatus | Finds Pets by status - - -# **find_pets_by_status** -> find_pets_by_status(req::HTTP.Request, status::FindPetsByStatusStatusParameter;) -> FindPetsByStatus200Response - -Finds Pets by status - -Multiple status values can be provided with comma separated strings - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**status** | [**FindPetsByStatusStatusParameter**](.md)| Status values that need to be considered for filter | [default to nothing] - -### Return type - -[**FindPetsByStatus200Response**](FindPetsByStatus200Response.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/test/deep_object/DeepServer/src/DeepServer.jl b/test/deep_object/DeepServer/src/DeepServer.jl deleted file mode 100644 index 750e3df..0000000 --- a/test/deep_object/DeepServer/src/DeepServer.jl +++ /dev/null @@ -1,54 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw""" -Encapsulates generated server code for DeepServer - -The following server methods must be implemented: - -- **find_pets_by_status** - - *invocation:* GET /pet/findByStatus - - *signature:* find_pets_by_status(req::HTTP.Request, status::FindPetsByStatusStatusParameter;) -> FindPetsByStatus200Response -""" -module DeepServer - -using HTTP -using URIs -using Dates -using TimeZones -using OpenAPI -using OpenAPI.Servers - -const API_VERSION = "1.0.0" - -include("modelincludes.jl") - -include("apis/api_PetApi.jl") - -""" -Register handlers for all APIs in this module in the supplied `Router` instance. - -Paramerets: -- `router`: Router to register handlers in -- `impl`: module that implements the server methods - -Optional parameters: -- `path_prefix`: prefix to be applied to all paths -- `optional_middlewares`: Register one or more optional middlewares to be applied to all requests. - -Optional middlewares can be one or more of: - - `init`: called before the request is processed - - `pre_validation`: called after the request is parsed but before validation - - `pre_invoke`: called after validation but before the handler is invoked - - `post_invoke`: called after the handler is invoked but before the response is sent - -The order in which middlewares are invoked are: -`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` -""" -function register(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - registerPetApi(router, impl; path_prefix=path_prefix, optional_middlewares...) - return router -end - -end # module DeepServer diff --git a/test/deep_object/DeepServer/src/apis/api_PetApi.jl b/test/deep_object/DeepServer/src/apis/api_PetApi.jl deleted file mode 100644 index 6c4f113..0000000 --- a/test/deep_object/DeepServer/src/apis/api_PetApi.jl +++ /dev/null @@ -1,37 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -function find_pets_by_status_read(handler) - function find_pets_by_status_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - query_params = HTTP.queryparams(URIs.URI(req.target)) - openapi_params["status"] = OpenAPI.Servers.to_param(FindPetsByStatusStatusParameter, query_params, "status", required=true, style="deepObject", is_explode=true) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function find_pets_by_status_validate(handler) - function find_pets_by_status_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - return handler(req) - end -end - -function find_pets_by_status_invoke(impl; post_invoke=nothing) - function find_pets_by_status_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.find_pets_by_status(req::HTTP.Request, openapi_params["status"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - - -function registerPetApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - HTTP.register!(router, "GET", path_prefix * "/pet/findByStatus", OpenAPI.Servers.middleware(impl, find_pets_by_status_read, find_pets_by_status_validate, find_pets_by_status_invoke; optional_middlewares...)) - return router -end diff --git a/test/deep_object/DeepServer/src/modelincludes.jl b/test/deep_object/DeepServer/src/modelincludes.jl deleted file mode 100644 index 74221b3..0000000 --- a/test/deep_object/DeepServer/src/modelincludes.jl +++ /dev/null @@ -1,5 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -include("models/model_FindPetsByStatus200Response.jl") -include("models/model_FindPetsByStatusStatusParameter.jl") diff --git a/test/deep_object/DeepServer/src/models/model_FindPetsByStatus200Response.jl b/test/deep_object/DeepServer/src/models/model_FindPetsByStatus200Response.jl deleted file mode 100644 index 2e493a1..0000000 --- a/test/deep_object/DeepServer/src/models/model_FindPetsByStatus200Response.jl +++ /dev/null @@ -1,29 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw""" - FindPetsByStatus200Response(; - result=nothing, - ) - - - result::FindPetsByStatusStatusParameter -""" -Base.@kwdef mutable struct FindPetsByStatus200Response <: OpenAPI.APIModel - result = nothing # spec type: Union{ Nothing, FindPetsByStatusStatusParameter } - - function FindPetsByStatus200Response(result, ) - OpenAPI.validate_property(FindPetsByStatus200Response, Symbol("result"), result) - return new(result, ) - end -end # type FindPetsByStatus200Response - -const _property_types_FindPetsByStatus200Response = Dict{Symbol,String}(Symbol("result")=>"FindPetsByStatusStatusParameter", ) -OpenAPI.property_type(::Type{ FindPetsByStatus200Response }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_FindPetsByStatus200Response[name]))} - -function check_required(o::FindPetsByStatus200Response) - true -end - -function OpenAPI.validate_property(::Type{ FindPetsByStatus200Response }, name::Symbol, val) -end diff --git a/test/deep_object/DeepServer/src/models/model_FindPetsByStatus200ResponseResult.jl b/test/deep_object/DeepServer/src/models/model_FindPetsByStatus200ResponseResult.jl deleted file mode 100644 index e543853..0000000 --- a/test/deep_object/DeepServer/src/models/model_FindPetsByStatus200ResponseResult.jl +++ /dev/null @@ -1,33 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw""" - FindPetsByStatus200ResponseResult(; - name=nothing, - statuses=nothing, - ) - - - name::String - - statuses::Vector{FindPetsByStatusStatusParameterStatusesInnerInner} -""" -Base.@kwdef mutable struct FindPetsByStatus200ResponseResult <: OpenAPI.APIModel - name::Union{Nothing, String} = nothing - statuses::Union{Nothing, Vector} = nothing # spec type: Union{ Nothing, Vector{FindPetsByStatusStatusParameterStatusesInnerInner} } - - function FindPetsByStatus200ResponseResult(name, statuses, ) - OpenAPI.validate_property(FindPetsByStatus200ResponseResult, Symbol("name"), name) - OpenAPI.validate_property(FindPetsByStatus200ResponseResult, Symbol("statuses"), statuses) - return new(name, statuses, ) - end -end # type FindPetsByStatus200ResponseResult - -const _property_types_FindPetsByStatus200ResponseResult = Dict{Symbol,String}(Symbol("name")=>"String", Symbol("statuses")=>"Vector{FindPetsByStatusStatusParameterStatusesInnerInner}", ) -OpenAPI.property_type(::Type{ FindPetsByStatus200ResponseResult }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_FindPetsByStatus200ResponseResult[name]))} - -function check_required(o::FindPetsByStatus200ResponseResult) - true -end - -function OpenAPI.validate_property(::Type{ FindPetsByStatus200ResponseResult }, name::Symbol, val) -end diff --git a/test/deep_object/DeepServer/src/models/model_FindPetsByStatusStatusParameter.jl b/test/deep_object/DeepServer/src/models/model_FindPetsByStatusStatusParameter.jl deleted file mode 100644 index e1903a1..0000000 --- a/test/deep_object/DeepServer/src/models/model_FindPetsByStatusStatusParameter.jl +++ /dev/null @@ -1,33 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw""" - FindPetsByStatusStatusParameter(; - name=nothing, - statuses=nothing, - ) - - - name::String - - statuses::Vector{String} -""" -Base.@kwdef mutable struct FindPetsByStatusStatusParameter <: OpenAPI.APIModel - name::Union{Nothing, String} = nothing - statuses::Union{Nothing, Vector{String}} = nothing - - function FindPetsByStatusStatusParameter(name, statuses, ) - OpenAPI.validate_property(FindPetsByStatusStatusParameter, Symbol("name"), name) - OpenAPI.validate_property(FindPetsByStatusStatusParameter, Symbol("statuses"), statuses) - return new(name, statuses, ) - end -end # type FindPetsByStatusStatusParameter - -const _property_types_FindPetsByStatusStatusParameter = Dict{Symbol,String}(Symbol("name")=>"String", Symbol("statuses")=>"Vector{String}", ) -OpenAPI.property_type(::Type{ FindPetsByStatusStatusParameter }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_FindPetsByStatusStatusParameter[name]))} - -function check_required(o::FindPetsByStatusStatusParameter) - true -end - -function OpenAPI.validate_property(::Type{ FindPetsByStatusStatusParameter }, name::Symbol, val) -end diff --git a/test/deep_object/DeepServer/src/models/model_FindPetsByStatusStatusParameterStatusesInner.jl b/test/deep_object/DeepServer/src/models/model_FindPetsByStatusStatusParameterStatusesInner.jl deleted file mode 100644 index 6b43755..0000000 --- a/test/deep_object/DeepServer/src/models/model_FindPetsByStatusStatusParameterStatusesInner.jl +++ /dev/null @@ -1,29 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw""" - FindPetsByStatusStatusParameterStatusesInner(; - type=nothing, - ) - - - type::String -""" -Base.@kwdef mutable struct FindPetsByStatusStatusParameterStatusesInner <: OpenAPI.APIModel - type::Union{Nothing, String} = nothing - - function FindPetsByStatusStatusParameterStatusesInner(type, ) - OpenAPI.validate_property(FindPetsByStatusStatusParameterStatusesInner, Symbol("type"), type) - return new(type, ) - end -end # type FindPetsByStatusStatusParameterStatusesInner - -const _property_types_FindPetsByStatusStatusParameterStatusesInner = Dict{Symbol,String}(Symbol("type")=>"String", ) -OpenAPI.property_type(::Type{ FindPetsByStatusStatusParameterStatusesInner }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_FindPetsByStatusStatusParameterStatusesInner[name]))} - -function check_required(o::FindPetsByStatusStatusParameterStatusesInner) - true -end - -function OpenAPI.validate_property(::Type{ FindPetsByStatusStatusParameterStatusesInner }, name::Symbol, val) -end diff --git a/test/deep_object/DeepServer/src/models/model_FindPetsByStatusStatusParameterStatusesInnerInner.jl b/test/deep_object/DeepServer/src/models/model_FindPetsByStatusStatusParameterStatusesInnerInner.jl deleted file mode 100644 index 39ed8d1..0000000 --- a/test/deep_object/DeepServer/src/models/model_FindPetsByStatusStatusParameterStatusesInnerInner.jl +++ /dev/null @@ -1,29 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw""" - FindPetsByStatusStatusParameterStatusesInnerInner(; - type=nothing, - ) - - - type::String -""" -Base.@kwdef mutable struct FindPetsByStatusStatusParameterStatusesInnerInner <: OpenAPI.APIModel - type::Union{Nothing, String} = nothing - - function FindPetsByStatusStatusParameterStatusesInnerInner(type, ) - OpenAPI.validate_property(FindPetsByStatusStatusParameterStatusesInnerInner, Symbol("type"), type) - return new(type, ) - end -end # type FindPetsByStatusStatusParameterStatusesInnerInner - -const _property_types_FindPetsByStatusStatusParameterStatusesInnerInner = Dict{Symbol,String}(Symbol("type")=>"String", ) -OpenAPI.property_type(::Type{ FindPetsByStatusStatusParameterStatusesInnerInner }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_FindPetsByStatusStatusParameterStatusesInnerInner[name]))} - -function check_required(o::FindPetsByStatusStatusParameterStatusesInnerInner) - true -end - -function OpenAPI.validate_property(::Type{ FindPetsByStatusStatusParameterStatusesInnerInner }, name::Symbol, val) -end diff --git a/test/deep_object/deep.yaml b/test/deep_object/deep.yaml deleted file mode 100644 index 46d3c43..0000000 --- a/test/deep_object/deep.yaml +++ /dev/null @@ -1,62 +0,0 @@ -openapi: 3.0.0 -servers: - - url: 'http://petstore.swagger.io/v2' -info: - description: >- - This is a sample server Petstore server. For this sample, you can use the api key - `special-key` to test the authorization filters. - version: 1.0.0 - title: OpenAPI Petstore - license: - name: Apache-2.0 - url: 'https://www.apache.org/licenses/LICENSE-2.0.html' -tags: - - name: pet - description: Everything about your Pets - - name: store - description: Access to Petstore orders - - name: user - description: Operations about user -paths: - /pet/findByStatus: - get: - tags: - - pet - summary: Finds Pets by status - description: Multiple status values can be provided with comma separated strings - operationId: findPetsByStatus - parameters: - - name: status - in: query - description: Status values that need to be considered for filter - required: true - style: deepObject - explode: true - deprecated: true - schema: - type: object - properties: - name: - type: string - statuses: - items: - type: string - responses: - '200': - description: successful operation - content: - application/json: - schema: - type: object - properties: - result: - type: object - properties: - name: - type: string - statuses: - items: - type: string - - '400': - description: Invalid status value diff --git a/test/deep_object/deep_client.jl b/test/deep_object/deep_client.jl deleted file mode 100644 index 59b56b6..0000000 --- a/test/deep_object/deep_client.jl +++ /dev/null @@ -1,25 +0,0 @@ -module DeepClientTest - -include("DeepClient/src/DeepClient.jl") -using .DeepClient -using .DeepClient.OpenAPI.Clients: Client -using .DeepClient: FindPetsByStatusStatusParameter - -using Test - -const server = "http://127.0.0.1:8081" - -function runtests(httplib::Symbol) - @info("DeepObject tests ($httplib backend)") - client = Client(server; httplib=httplib) - api = DeepClient.PetApi(client) - unsold = FindPetsByStatusStatusParameter("key", ["available", "pending"]) - resp, http_resp = find_pets_by_status(api, unsold) - @debug("deep object response", resp, http_resp) - res = resp.result - @test res.name == "key" - @test res.statuses == ["available", "pending"] - @test http_resp.status == 200 -end - -end # module DeepObjectClientTest diff --git a/test/deep_object/deep_server.jl b/test/deep_object/deep_server.jl deleted file mode 100644 index 2c02e8d..0000000 --- a/test/deep_object/deep_server.jl +++ /dev/null @@ -1,38 +0,0 @@ -module DeepServerTest -include("DeepServer/src/DeepServer.jl") -using .DeepServer -using HTTP -using .DeepServer: register, FindPetsByStatus200Response - -const server = Ref{Any}(nothing) - -function find_pets_by_status(::HTTP.Request, param::DeepServer.FindPetsByStatusStatusParameter) - return FindPetsByStatus200Response(param) -end - -function stop(::HTTP.Request) - HTTP.close(server[]) - return HTTP.Response(200, "") -end - -function ping(::HTTP.Request) - return HTTP.Response(200, "") -end - -function run_server(port=8081) - try - @info "Running deepserver" - router = HTTP.Router() - HTTP.register!(router, "GET", "/stop", stop) - HTTP.register!(router, "GET", "/ping", ping) - router = register(router, @__MODULE__) - server[] = HTTP.serve!(router, port) - @info "wait deepserver" - wait(server[]) - catch ex - @error("Server error", exception=(ex, catch_backtrace())) - end -end - -end # module DeepObjectClientTest -DeepServerTest.run_server() diff --git a/test/discriminators.jl b/test/discriminators.jl new file mode 100644 index 0000000..7502070 --- /dev/null +++ b/test/discriminators.jl @@ -0,0 +1,265 @@ +@testset "discriminators and generated-name collisions" begin + @testset "explicit and default discriminator mappings" begin + document = minimal_openapi( + "3.2.0", + OpenAPI.obj( + "/pets" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "getPet", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "\$ref" => "#/components/schemas/Pet", + ), + ), + ), + ), + ), + ), + ), + ), + ) + document["components"] = OpenAPI.obj( + "schemas" => OpenAPI.obj( + "Cat" => OpenAPI.obj( + "type" => "object", + "required" => ["kind", "meows"], + "properties" => OpenAPI.obj( + "kind" => OpenAPI.obj( + "type" => "string", + "enum" => ["cat", "feline", "Cat"], + ), + "meows" => OpenAPI.obj("type" => "boolean"), + ), + "additionalProperties" => false, + ), + "Dog" => OpenAPI.obj( + "type" => "object", + "required" => ["kind", "barks"], + "properties" => OpenAPI.obj( + "kind" => OpenAPI.obj( + "type" => "string", + "enum" => ["dog", "canine", "Dog"], + ), + "barks" => OpenAPI.obj("type" => "boolean"), + ), + "additionalProperties" => false, + ), + "OtherPet" => OpenAPI.obj( + "type" => "object", + "required" => ["kind", "note"], + "properties" => OpenAPI.obj( + "kind" => OpenAPI.obj("type" => "string"), + "note" => OpenAPI.obj("type" => "string"), + ), + "additionalProperties" => false, + ), + "Pet" => OpenAPI.obj( + "oneOf" => Any[ + OpenAPI.obj("\$ref" => "#/components/schemas/Cat"), + OpenAPI.obj("\$ref" => "#/components/schemas/Dog"), + OpenAPI.obj("\$ref" => "#/components/schemas/OtherPet"), + ], + "discriminator" => OpenAPI.obj( + "propertyName" => "kind", + "mapping" => OpenAPI.obj( + "feline" => "#/components/schemas/Cat", + "canine" => "#/components/schemas/Dog", + ), + "defaultMapping" => "#/components/schemas/OtherPet", + ), + ), + ), + ) + + source = OpenAPI.client(document; name = "DiscriminatorClient") + host = Module(:DiscriminatorClientHost) + Base.include_string(host, source, "DiscriminatorClient.jl") + client_module = Base.invokelatest(getfield, host, :DiscriminatorClient) + response_media = only(only(client_module._OP_getpet.responses).media) + + decode_pet(json) = Base.invokelatest( + client_module._decode_body, + client_module.DEFAULT_CLIENT, + client_module.Pet, + "application/json", + Vector{UInt8}(codeunits(json)), + response_media[3], + ) + cat = decode_pet("{\"kind\":\"feline\",\"meows\":true}") + dog = decode_pet("{\"kind\":\"canine\",\"barks\":true}") + other = decode_pet("{\"kind\":\"iguana\",\"note\":\"quiet\"}") + implicit_cat = decode_pet("{\"kind\":\"Cat\",\"meows\":true}") + implicit_dog = decode_pet("{\"kind\":\"Dog\",\"barks\":true}") + @test cat.value isa client_module.Cat + @test cat.value.meows + @test dog.value isa client_module.Dog + @test dog.value.barks + @test other.value isa client_module.OtherPet + @test other.value.note == "quiet" + @test implicit_cat.value isa client_module.Cat + @test implicit_dog.value isa client_module.Dog + + bad = deepcopy(document) + bad["components"]["schemas"]["Pet"]["discriminator"]["mapping"]["bad"] = + "#/components/schemas/Missing" + error = @test_throws OpenAPI.OpenAPIError OpenAPI.plan(bad) + @test :invalid_discriminator_mapping in + Set(diagnostic.code for diagnostic in error.value.diagnostics) + end + + @testset "implicit mappings and collision-safe Julia names" begin + document = minimal_openapi( + "3.1.1", + OpenAPI.obj( + "/value/{client}" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "module", + "parameters" => Any[ + OpenAPI.obj( + "name" => "client", + "in" => "path", + "required" => true, + "schema" => OpenAPI.obj("type" => "string"), + ), + OpenAPI.obj( + "name" => "foo-bar", + "in" => "query", + "schema" => OpenAPI.obj("type" => "string"), + ), + OpenAPI.obj( + "name" => "foo_bar", + "in" => "query", + "schema" => OpenAPI.obj("type" => "string"), + ), + ], + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "value", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "\$ref" => "#/components/schemas/ImplicitPet", + ), + ), + ), + ), + ), + ), + ), + ), + ) + document["components"] = OpenAPI.obj( + "schemas" => OpenAPI.obj( + "Cat" => OpenAPI.obj( + "type" => "object", + "required" => ["kind", "foo-bar", "foo_bar"], + "properties" => OpenAPI.obj( + "kind" => OpenAPI.obj("const" => "Cat"), + "foo-bar" => OpenAPI.obj("type" => "string"), + "foo_bar" => OpenAPI.obj("type" => "integer"), + ), + "additionalProperties" => false, + ), + "Dog" => OpenAPI.obj( + "type" => "object", + "required" => ["kind"], + "properties" => OpenAPI.obj( + "kind" => OpenAPI.obj("const" => "Dog"), + ), + "additionalProperties" => false, + ), + "ImplicitPet" => OpenAPI.obj( + "oneOf" => Any[ + OpenAPI.obj("\$ref" => "#/components/schemas/Cat"), + OpenAPI.obj("\$ref" => "#/components/schemas/Dog"), + ], + "discriminator" => OpenAPI.obj("propertyName" => "kind"), + ), + ), + ) + + plan = OpenAPI.plan(document; name = "CollisionClient") + operation = only(plan.operations) + @test operation.name == "module_" + @test [parameter.name for parameter in operation.parameters] == + ["client_2", "foo_bar", "foo_bar_2"] + cat_plan = only(model for model in plan.models if model.name == "Cat") + @test [field.name for field in cat_plan.fields] == + ["kind", "foo_bar", "foo_bar_2"] + @test [field.wire_name for field in cat_plan.fields] == + ["kind", "foo-bar", "foo_bar"] + + source = OpenAPI.client(plan) + host = Module(:CollisionClientHost) + Base.include_string(host, source, "CollisionClient.jl") + client_module = Base.invokelatest(getfield, host, :CollisionClient) + pet = Base.invokelatest( + client_module._decode, + client_module.ImplicitPet, + Dict("kind" => "Cat", "foo-bar" => "x", "foo_bar" => 7), + ) + @test pet.value isa client_module.Cat + @test pet.value.foo_bar == "x" + @test pet.value.foo_bar_2 == 7 + end + + @testset "forward recursive union declarations" begin + document = minimal_openapi( + "3.1.1", + OpenAPI.obj( + "/errors" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "errors", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "errors", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "\$ref" => "#/components/schemas/ErrorDetails", + ), + ), + ), + ), + ), + ), + ), + ), + ) + document["components"] = OpenAPI.obj( + "schemas" => OpenAPI.obj( + "ErrorDetails" => OpenAPI.obj( + "oneOf" => Any[ + OpenAPI.obj( + "type" => "object", + "additionalProperties" => OpenAPI.obj( + "\$ref" => "#/components/schemas/ErrorDetails", + ), + ), + OpenAPI.obj("type" => "string"), + ], + ), + ), + ) + source = OpenAPI.client(document; name = "RecursiveUnionClient") + host = Module(:RecursiveUnionClientHost) + Base.include_string(host, source, "RecursiveUnionClient.jl") + client_module = Base.invokelatest( + getfield, + host, + :RecursiveUnionClient, + ) + value = Base.invokelatest( + client_module._decode, + client_module.ErrorDetails, + Dict("nested" => "leaf"), + ) + @test value.value isa client_module.ErrorDetails1 + @test value.value.additional_properties["nested"] isa + client_module.ErrorDetails + @test value.value.additional_properties["nested"].value == "leaf" + end +end diff --git a/test/forms/FormsClient/.openapi-generator-ignore b/test/forms/FormsClient/.openapi-generator-ignore deleted file mode 100644 index 7484ee5..0000000 --- a/test/forms/FormsClient/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/test/forms/FormsClient/.openapi-generator/FILES b/test/forms/FormsClient/.openapi-generator/FILES deleted file mode 100644 index 4cd5261..0000000 --- a/test/forms/FormsClient/.openapi-generator/FILES +++ /dev/null @@ -1,7 +0,0 @@ -README.md -docs/DefaultApi.md -docs/TestResponse.md -src/FormsClient.jl -src/apis/api_DefaultApi.jl -src/modelincludes.jl -src/models/model_TestResponse.jl diff --git a/test/forms/FormsClient/.openapi-generator/VERSION b/test/forms/FormsClient/.openapi-generator/VERSION deleted file mode 100644 index 96cfbb1..0000000 --- a/test/forms/FormsClient/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.13.0-SNAPSHOT diff --git a/test/forms/FormsClient/README.md b/test/forms/FormsClient/README.md deleted file mode 100644 index b7cf708..0000000 --- a/test/forms/FormsClient/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# Julia API client for FormsClient - -Tests for different types of POST operations with forms and file uploads - -## Overview -This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. - -- API version: 0.1.0 -- Generator version: 7.13.0-SNAPSHOT -- Build package: org.openapitools.codegen.languages.JuliaClientCodegen - - -## Installation -Place the Julia files generated under the `src` folder in your Julia project. Include FormsClient.jl in the project code. -It would include the module named FormsClient. - -Documentation is generated as markdown files under the `docs` folder. You can include them in your project documentation. -Documentation is also embedded in Julia which can be used with a Julia specific documentation generator. - -## API Endpoints - -Class | Method ------------- | ------------- -*DefaultApi* | [**post_urlencoded_form**](docs/DefaultApi.md#post_urlencoded_form)
**POST** /test/{form_id}/post_urlencoded_form_data
posts a urlencoded form, with file contents and additional metadata, both of which are strings -*DefaultApi* | [**upload_binary_file**](docs/DefaultApi.md#upload_binary_file)
**POST** /test/{file_id}/upload_binary_file
uploads a binary file given its path, along with some metadata -*DefaultApi* | [**upload_text_file**](docs/DefaultApi.md#upload_text_file)
**POST** /test/{file_id}/upload_text_file
uploads text file contents along with some metadata - - -## Models - - - [TestResponse](docs/TestResponse.md) - - - -## Authorization -Endpoints do not require authorization. - - -## Author - - - diff --git a/test/forms/FormsClient/docs/DefaultApi.md b/test/forms/FormsClient/docs/DefaultApi.md deleted file mode 100644 index 9101942..0000000 --- a/test/forms/FormsClient/docs/DefaultApi.md +++ /dev/null @@ -1,116 +0,0 @@ -# DefaultApi - -All URIs are relative to *http://localhost* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**post_urlencoded_form**](DefaultApi.md#post_urlencoded_form) | **POST** /test/{form_id}/post_urlencoded_form_data | posts a urlencoded form, with file contents and additional metadata, both of which are strings -[**upload_binary_file**](DefaultApi.md#upload_binary_file) | **POST** /test/{file_id}/upload_binary_file | uploads a binary file given its path, along with some metadata -[**upload_text_file**](DefaultApi.md#upload_text_file) | **POST** /test/{file_id}/upload_text_file | uploads text file contents along with some metadata - - -# **post_urlencoded_form** -> post_urlencoded_form(_api::DefaultApi, form_id::Int64, file::String; additional_metadata=nothing, _mediaType=nothing) -> TestResponse, OpenAPI.Clients.ApiResponse
-> post_urlencoded_form(_api::DefaultApi, response_stream::Channel, form_id::Int64, file::String; additional_metadata=nothing, _mediaType=nothing) -> Channel{ TestResponse }, OpenAPI.Clients.ApiResponse - -posts a urlencoded form, with file contents and additional metadata, both of which are strings - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **DefaultApi** | API context | -**form_id** | **Int64** | ID of form to update | -**file** | **String** | file contents to upload, in string format | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **additional_metadata** | **String** | Additional data to pass to server | [default to nothing] - -### Return type - -[**TestResponse**](TestResponse.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/x-www-form-urlencoded - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **upload_binary_file** -> upload_binary_file(_api::DefaultApi, file_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) -> TestResponse, OpenAPI.Clients.ApiResponse
-> upload_binary_file(_api::DefaultApi, response_stream::Channel, file_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) -> Channel{ TestResponse }, OpenAPI.Clients.ApiResponse - -uploads a binary file given its path, along with some metadata - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **DefaultApi** | API context | -**file_id** | **Int64** | ID of file to update | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **additional_metadata** | **String** | Additional data to pass to server | [default to nothing] - **file** | **String** | file to upload, must be a string representing a valid file path | - -### Return type - -[**TestResponse**](TestResponse.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: multipart/form-data - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - -# **upload_text_file** -> upload_text_file(_api::DefaultApi, file_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) -> TestResponse, OpenAPI.Clients.ApiResponse
-> upload_text_file(_api::DefaultApi, response_stream::Channel, file_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) -> Channel{ TestResponse }, OpenAPI.Clients.ApiResponse - -uploads text file contents along with some metadata - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **_api** | **DefaultApi** | API context | -**file_id** | **Int64** | ID of file to update | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **additional_metadata** | **String** | Additional data to pass to server, a string | [default to nothing] - **file** | **String** | file contents to upload in base64 encoded format | [default to nothing] - -### Return type - -[**TestResponse**](TestResponse.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: multipart/form-data - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - diff --git a/test/forms/FormsClient/docs/TestResponse.md b/test/forms/FormsClient/docs/TestResponse.md deleted file mode 100644 index 3f97767..0000000 --- a/test/forms/FormsClient/docs/TestResponse.md +++ /dev/null @@ -1,12 +0,0 @@ -# TestResponse - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**message** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/forms/FormsClient/src/FormsClient.jl b/test/forms/FormsClient/src/FormsClient.jl deleted file mode 100644 index 8cb2c7c..0000000 --- a/test/forms/FormsClient/src/FormsClient.jl +++ /dev/null @@ -1,16 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -module FormsClient - -using Dates, TimeZones -using OpenAPI -using OpenAPI.Clients - -const API_VERSION = "0.1.0" - -include("modelincludes.jl") - -include("apis/api_DefaultApi.jl") - -end # module FormsClient diff --git a/test/forms/FormsClient/src/apis/api_DefaultApi.jl b/test/forms/FormsClient/src/apis/api_DefaultApi.jl deleted file mode 100644 index e937581..0000000 --- a/test/forms/FormsClient/src/apis/api_DefaultApi.jl +++ /dev/null @@ -1,115 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -struct DefaultApi <: OpenAPI.APIClientImpl - client::OpenAPI.Clients.Client -end - -""" -The default API base path for APIs in `DefaultApi`. -This can be used to construct the `OpenAPI.Clients.Client` instance. -""" -basepath(::Type{ DefaultApi }) = "http://localhost" - -const _returntypes_post_urlencoded_form_DefaultApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => TestResponse, -) - -function _oacinternal_post_urlencoded_form(_api::DefaultApi, form_id::Int64, file::String; additional_metadata=nothing, _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_post_urlencoded_form_DefaultApi, "/test/{form_id}/post_urlencoded_form_data", []) - OpenAPI.Clients.set_param(_ctx.path, "form_id", form_id) # type Int64 - OpenAPI.Clients.set_param(_ctx.form, "additionalMetadata", additional_metadata) # type String - OpenAPI.Clients.set_param(_ctx.form, "file", file) # type String - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/x-www-form-urlencoded", ] : [_mediaType]) - return _ctx -end - -@doc raw"""posts a urlencoded form, with file contents and additional metadata, both of which are strings - -Params: -- form_id::Int64 (required) -- file::String (required) -- additional_metadata::String - -Return: TestResponse, OpenAPI.Clients.ApiResponse -""" -function post_urlencoded_form(_api::DefaultApi, form_id::Int64, file::String; additional_metadata=nothing, _mediaType=nothing) - _ctx = _oacinternal_post_urlencoded_form(_api, form_id, file; additional_metadata=additional_metadata, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function post_urlencoded_form(_api::DefaultApi, response_stream::Channel, form_id::Int64, file::String; additional_metadata=nothing, _mediaType=nothing) - _ctx = _oacinternal_post_urlencoded_form(_api, form_id, file; additional_metadata=additional_metadata, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_upload_binary_file_DefaultApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => TestResponse, -) - -function _oacinternal_upload_binary_file(_api::DefaultApi, file_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_upload_binary_file_DefaultApi, "/test/{file_id}/upload_binary_file", []) - OpenAPI.Clients.set_param(_ctx.path, "file_id", file_id) # type Int64 - OpenAPI.Clients.set_param(_ctx.form, "additionalMetadata", additional_metadata) # type String - OpenAPI.Clients.set_param(_ctx.file, "file", file) # type String - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["multipart/form-data", ] : [_mediaType]) - return _ctx -end - -@doc raw"""uploads a binary file given its path, along with some metadata - -Params: -- file_id::Int64 (required) -- additional_metadata::String -- file::String - -Return: TestResponse, OpenAPI.Clients.ApiResponse -""" -function upload_binary_file(_api::DefaultApi, file_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) - _ctx = _oacinternal_upload_binary_file(_api, file_id; additional_metadata=additional_metadata, file=file, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function upload_binary_file(_api::DefaultApi, response_stream::Channel, file_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) - _ctx = _oacinternal_upload_binary_file(_api, file_id; additional_metadata=additional_metadata, file=file, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_upload_text_file_DefaultApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => TestResponse, -) - -function _oacinternal_upload_text_file(_api::DefaultApi, file_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_upload_text_file_DefaultApi, "/test/{file_id}/upload_text_file", []) - OpenAPI.Clients.set_param(_ctx.path, "file_id", file_id) # type Int64 - OpenAPI.Clients.set_param(_ctx.form, "additionalMetadata", additional_metadata) # type String - OpenAPI.Clients.set_param(_ctx.form, "file", file) # type String - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["multipart/form-data", ] : [_mediaType]) - return _ctx -end - -@doc raw"""uploads text file contents along with some metadata - -Params: -- file_id::Int64 (required) -- additional_metadata::String -- file::String - -Return: TestResponse, OpenAPI.Clients.ApiResponse -""" -function upload_text_file(_api::DefaultApi, file_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) - _ctx = _oacinternal_upload_text_file(_api, file_id; additional_metadata=additional_metadata, file=file, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function upload_text_file(_api::DefaultApi, response_stream::Channel, file_id::Int64; additional_metadata=nothing, file=nothing, _mediaType=nothing) - _ctx = _oacinternal_upload_text_file(_api, file_id; additional_metadata=additional_metadata, file=file, _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -export post_urlencoded_form -export upload_binary_file -export upload_text_file diff --git a/test/forms/FormsClient/src/modelincludes.jl b/test/forms/FormsClient/src/modelincludes.jl deleted file mode 100644 index 9834a2e..0000000 --- a/test/forms/FormsClient/src/modelincludes.jl +++ /dev/null @@ -1,4 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -include("models/model_TestResponse.jl") diff --git a/test/forms/FormsClient/src/models/model_TestResponse.jl b/test/forms/FormsClient/src/models/model_TestResponse.jl deleted file mode 100644 index a7dfca7..0000000 --- a/test/forms/FormsClient/src/models/model_TestResponse.jl +++ /dev/null @@ -1,36 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""TestResponse - - TestResponse(; - message=nothing, - ) - - - message::String -""" -Base.@kwdef mutable struct TestResponse <: OpenAPI.APIModel - message::Union{Nothing, String} = nothing - - function TestResponse(message, ) - o = new(message, ) - OpenAPI.validate_properties(o) - return o - end -end # type TestResponse - -const _property_types_TestResponse = Dict{Symbol,String}(Symbol("message")=>"String", ) -OpenAPI.property_type(::Type{ TestResponse }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_TestResponse[name]))} - -function OpenAPI.check_required(o::TestResponse) - true -end - -function OpenAPI.validate_properties(o::TestResponse) - OpenAPI.validate_property(TestResponse, Symbol("message"), o.message) -end - -function OpenAPI.validate_property(::Type{ TestResponse }, name::Symbol, val) - -end diff --git a/test/forms/FormsServer/.openapi-generator-ignore b/test/forms/FormsServer/.openapi-generator-ignore deleted file mode 100644 index 7484ee5..0000000 --- a/test/forms/FormsServer/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/test/forms/FormsServer/.openapi-generator/FILES b/test/forms/FormsServer/.openapi-generator/FILES deleted file mode 100644 index f036474..0000000 --- a/test/forms/FormsServer/.openapi-generator/FILES +++ /dev/null @@ -1,7 +0,0 @@ -README.md -docs/DefaultApi.md -docs/TestResponse.md -src/FormsServer.jl -src/apis/api_DefaultApi.jl -src/modelincludes.jl -src/models/model_TestResponse.jl diff --git a/test/forms/FormsServer/.openapi-generator/VERSION b/test/forms/FormsServer/.openapi-generator/VERSION deleted file mode 100644 index 96cfbb1..0000000 --- a/test/forms/FormsServer/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.13.0-SNAPSHOT diff --git a/test/forms/FormsServer/README.md b/test/forms/FormsServer/README.md deleted file mode 100644 index b287034..0000000 --- a/test/forms/FormsServer/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# Julia API server for FormsServer - -Tests for different types of POST operations with forms and file uploads - -## Overview -This API server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. - -- API version: 0.1.0 -- Generator version: 7.13.0-SNAPSHOT -- Build package: org.openapitools.codegen.languages.JuliaServerCodegen - - -## Installation -Place the Julia files generated under the `src` folder in your Julia project. Include FormsServer.jl in the project code. -It would include the module named FormsServer. - -Implement the server methods as listed below. They are also documented with the FormsServer module. -Launch a HTTP server with a router that has all handlers registered. A `register` method is provided in FormsServer module for convenience. - -```julia -register( - router::HTTP.Router, # Router to register handlers in - impl; # Module that implements the server methods - path_prefix::String="", # Prefix to be applied to all paths - optional_middlewares... # Optional middlewares to be applied to all handlers -) -``` - -Optional middlewares can be one or more of: -- `init`: called before the request is processed -- `pre_validation`: called after the request is parsed but before validation -- `pre_invoke`: called after validation but before the handler is invoked -- `post_invoke`: called after the handler is invoked but before the response is sent - -The order in which middlewares are invoked are: -`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` - - -## API Endpoints - -The following server methods must be implemented: - -Class | Method | HTTP request | Description ------------- | ------------- | ------------- | ------------- -*DefaultApi* | [**post_urlencoded_form**](docs/DefaultApi.md#post_urlencoded_form) | **POST** /test/{form_id}/post_urlencoded_form_data | posts a urlencoded form, with file contents and additional metadata, both of which are strings -*DefaultApi* | [**upload_binary_file**](docs/DefaultApi.md#upload_binary_file) | **POST** /test/{file_id}/upload_binary_file | uploads a binary file given its path, along with some metadata -*DefaultApi* | [**upload_text_file**](docs/DefaultApi.md#upload_text_file) | **POST** /test/{file_id}/upload_text_file | uploads text file contents along with some metadata - - - -## Models - - - [TestResponse](docs/TestResponse.md) - - - -## Author - - - diff --git a/test/forms/FormsServer/docs/DefaultApi.md b/test/forms/FormsServer/docs/DefaultApi.md deleted file mode 100644 index b677d84..0000000 --- a/test/forms/FormsServer/docs/DefaultApi.md +++ /dev/null @@ -1,113 +0,0 @@ -# DefaultApi - -All URIs are relative to *http://localhost* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**post_urlencoded_form**](DefaultApi.md#post_urlencoded_form) | **POST** /test/{form_id}/post_urlencoded_form_data | posts a urlencoded form, with file contents and additional metadata, both of which are strings -[**upload_binary_file**](DefaultApi.md#upload_binary_file) | **POST** /test/{file_id}/upload_binary_file | uploads a binary file given its path, along with some metadata -[**upload_text_file**](DefaultApi.md#upload_text_file) | **POST** /test/{file_id}/upload_text_file | uploads text file contents along with some metadata - - -# **post_urlencoded_form** -> post_urlencoded_form(req::HTTP.Request, form_id::Int64, file::String; additional_metadata=nothing,) -> TestResponse - -posts a urlencoded form, with file contents and additional metadata, both of which are strings - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**form_id** | **Int64**| ID of form to update | -**file** | **String**| file contents to upload, in string format | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **additional_metadata** | **String**| Additional data to pass to server | [default to nothing] - -### Return type - -[**TestResponse**](TestResponse.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/x-www-form-urlencoded - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **upload_binary_file** -> upload_binary_file(req::HTTP.Request, file_id::Int64; additional_metadata=nothing, file=nothing,) -> TestResponse - -uploads a binary file given its path, along with some metadata - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**file_id** | **Int64**| ID of file to update | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **additional_metadata** | **String**| Additional data to pass to server | [default to nothing] - **file** | **Vector{UInt8}**| file to upload, must be a string representing a valid file path | - -### Return type - -[**TestResponse**](TestResponse.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: multipart/form-data - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **upload_text_file** -> upload_text_file(req::HTTP.Request, file_id::Int64; additional_metadata=nothing, file=nothing,) -> TestResponse - -uploads text file contents along with some metadata - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**file_id** | **Int64**| ID of file to update | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **additional_metadata** | **String**| Additional data to pass to server, a string | [default to nothing] - **file** | **String**| file contents to upload in base64 encoded format | [default to nothing] - -### Return type - -[**TestResponse**](TestResponse.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: multipart/form-data - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/test/forms/FormsServer/docs/TestResponse.md b/test/forms/FormsServer/docs/TestResponse.md deleted file mode 100644 index 3f97767..0000000 --- a/test/forms/FormsServer/docs/TestResponse.md +++ /dev/null @@ -1,12 +0,0 @@ -# TestResponse - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**message** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/forms/FormsServer/src/FormsServer.jl b/test/forms/FormsServer/src/FormsServer.jl deleted file mode 100644 index eb81503..0000000 --- a/test/forms/FormsServer/src/FormsServer.jl +++ /dev/null @@ -1,60 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw""" -Encapsulates generated server code for FormsServer - -The following server methods must be implemented: - -- **post_urlencoded_form** - - *invocation:* POST /test/{form_id}/post_urlencoded_form_data - - *signature:* post_urlencoded_form(req::HTTP.Request, form_id::Int64, file::String; additional_metadata=nothing,) -> TestResponse -- **upload_binary_file** - - *invocation:* POST /test/{file_id}/upload_binary_file - - *signature:* upload_binary_file(req::HTTP.Request, file_id::Int64; additional_metadata=nothing, file=nothing,) -> TestResponse -- **upload_text_file** - - *invocation:* POST /test/{file_id}/upload_text_file - - *signature:* upload_text_file(req::HTTP.Request, file_id::Int64; additional_metadata=nothing, file=nothing,) -> TestResponse -""" -module FormsServer - -using HTTP -using URIs -using Dates -using TimeZones -using OpenAPI -using OpenAPI.Servers - -const API_VERSION = "0.1.0" - -include("modelincludes.jl") - -include("apis/api_DefaultApi.jl") - -""" -Register handlers for all APIs in this module in the supplied `Router` instance. - -Paramerets: -- `router`: Router to register handlers in -- `impl`: module that implements the server methods - -Optional parameters: -- `path_prefix`: prefix to be applied to all paths -- `optional_middlewares`: Register one or more optional middlewares to be applied to all requests. - -Optional middlewares can be one or more of: - - `init`: called before the request is processed - - `pre_validation`: called after the request is parsed but before validation - - `pre_invoke`: called after validation but before the handler is invoked - - `post_invoke`: called after the handler is invoked but before the response is sent - -The order in which middlewares are invoked are: -`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` -""" -function register(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - registerDefaultApi(router, impl; path_prefix=path_prefix, optional_middlewares...) - return router -end - -end # module FormsServer diff --git a/test/forms/FormsServer/src/apis/api_DefaultApi.jl b/test/forms/FormsServer/src/apis/api_DefaultApi.jl deleted file mode 100644 index 8290a13..0000000 --- a/test/forms/FormsServer/src/apis/api_DefaultApi.jl +++ /dev/null @@ -1,213 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -function post_urlencoded_form_read(handler) - function post_urlencoded_form_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["form_id"] = OpenAPI.Servers.to_param(Int64, path_params, "form_id", required=true, ) - ismultipart = false - form_data = ismultipart ? HTTP.parse_multipart_form(req) : HTTP.queryparams(String(copy(req.body))) - openapi_params["additionalMetadata"] = OpenAPI.Servers.to_param(String, form_data, "additionalMetadata"; multipart=ismultipart, isfile=false, ) - openapi_params["file"] = OpenAPI.Servers.to_param(String, form_data, "file"; multipart=ismultipart, isfile=false, required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function post_urlencoded_form_validate(handler) - function post_urlencoded_form_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "post_urlencoded_form" - - n = "form_id" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "file" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "additionalMetadata" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function post_urlencoded_form_invoke(impl; post_invoke=nothing) - function post_urlencoded_form_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.post_urlencoded_form(req::HTTP.Request, openapi_params["form_id"], openapi_params["file"]; additional_metadata=get(openapi_params, "additionalMetadata", nothing),) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function upload_binary_file_read(handler) - function upload_binary_file_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["file_id"] = OpenAPI.Servers.to_param(Int64, path_params, "file_id", required=true, ) - ismultipart = true - form_data = ismultipart ? HTTP.parse_multipart_form(req) : HTTP.queryparams(String(copy(req.body))) - openapi_params["additionalMetadata"] = OpenAPI.Servers.to_param(String, form_data, "additionalMetadata"; multipart=ismultipart, isfile=false, ) - openapi_params["file"] = OpenAPI.Servers.to_param(Vector{UInt8}, form_data, "file"; multipart=ismultipart, isfile=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function upload_binary_file_validate(handler) - function upload_binary_file_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "upload_binary_file" - - n = "file_id" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "additionalMetadata" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "file" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function upload_binary_file_invoke(impl; post_invoke=nothing) - function upload_binary_file_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.upload_binary_file(req::HTTP.Request, openapi_params["file_id"]; additional_metadata=get(openapi_params, "additionalMetadata", nothing), file=get(openapi_params, "file", nothing),) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function upload_text_file_read(handler) - function upload_text_file_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["file_id"] = OpenAPI.Servers.to_param(Int64, path_params, "file_id", required=true, ) - ismultipart = true - form_data = ismultipart ? HTTP.parse_multipart_form(req) : HTTP.queryparams(String(copy(req.body))) - openapi_params["additionalMetadata"] = OpenAPI.Servers.to_param(String, form_data, "additionalMetadata"; multipart=ismultipart, isfile=false, ) - openapi_params["file"] = OpenAPI.Servers.to_param(String, form_data, "file"; multipart=ismultipart, isfile=false, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function upload_text_file_validate(handler) - function upload_text_file_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "upload_text_file" - - n = "file_id" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "additionalMetadata" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "file" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function upload_text_file_invoke(impl; post_invoke=nothing) - function upload_text_file_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.upload_text_file(req::HTTP.Request, openapi_params["file_id"]; additional_metadata=get(openapi_params, "additionalMetadata", nothing), file=get(openapi_params, "file", nothing),) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - - -function registerDefaultApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - HTTP.register!(router, "POST", path_prefix * "/test/{form_id}/post_urlencoded_form_data", OpenAPI.Servers.middleware(impl, post_urlencoded_form_read, post_urlencoded_form_validate, post_urlencoded_form_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/test/{file_id}/upload_binary_file", OpenAPI.Servers.middleware(impl, upload_binary_file_read, upload_binary_file_validate, upload_binary_file_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/test/{file_id}/upload_text_file", OpenAPI.Servers.middleware(impl, upload_text_file_read, upload_text_file_validate, upload_text_file_invoke; optional_middlewares...)) - return router -end diff --git a/test/forms/FormsServer/src/modelincludes.jl b/test/forms/FormsServer/src/modelincludes.jl deleted file mode 100644 index 9834a2e..0000000 --- a/test/forms/FormsServer/src/modelincludes.jl +++ /dev/null @@ -1,4 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -include("models/model_TestResponse.jl") diff --git a/test/forms/FormsServer/src/models/model_TestResponse.jl b/test/forms/FormsServer/src/models/model_TestResponse.jl deleted file mode 100644 index a7dfca7..0000000 --- a/test/forms/FormsServer/src/models/model_TestResponse.jl +++ /dev/null @@ -1,36 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""TestResponse - - TestResponse(; - message=nothing, - ) - - - message::String -""" -Base.@kwdef mutable struct TestResponse <: OpenAPI.APIModel - message::Union{Nothing, String} = nothing - - function TestResponse(message, ) - o = new(message, ) - OpenAPI.validate_properties(o) - return o - end -end # type TestResponse - -const _property_types_TestResponse = Dict{Symbol,String}(Symbol("message")=>"String", ) -OpenAPI.property_type(::Type{ TestResponse }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_TestResponse[name]))} - -function OpenAPI.check_required(o::TestResponse) - true -end - -function OpenAPI.validate_properties(o::TestResponse) - OpenAPI.validate_property(TestResponse, Symbol("message"), o.message) -end - -function OpenAPI.validate_property(::Type{ TestResponse }, name::Symbol, val) - -end diff --git a/test/forms/forms_client.jl b/test/forms/forms_client.jl deleted file mode 100644 index fd96b71..0000000 --- a/test/forms/forms_client.jl +++ /dev/null @@ -1,48 +0,0 @@ -module FormsV3Client - -include("FormsClient/src/FormsClient.jl") -using .FormsClient -using Test -using OpenAPI -using OpenAPI.Clients -import OpenAPI.Clients: Client -using Base64 - -const server = "http://127.0.0.1:8081" - -function test(uri, httplib::Symbol) - @info("FormsClient.DefaultApi ($httplib backend)") - client = Client(uri; httplib=httplib) - api = FormsClient.DefaultApi(client) - - mktemp() do test_file_path, test_file_io - file_contents = "file contents" - print(test_file_io, file_contents) - close(test_file_io) - - api_return, http_resp = FormsClient.post_urlencoded_form(api, 1, file_contents; additional_metadata="my metadata") - @test isa(api_return, FormsClient.TestResponse) - @test api_return.message == "success, form_id=1, metadata=my metadata, file=file contents" - @test http_resp.status == 200 - - api_return, http_resp = FormsClient.upload_binary_file(api, 1; additional_metadata="my metadata", file=test_file_path) - @test isa(api_return, FormsClient.TestResponse) - @test api_return.message == "success, file_id=1, metadata=my metadata, file=file contents" - @test http_resp.status == 200 - - api_return, http_resp = FormsClient.upload_text_file(api, 1; additional_metadata="my metadata", file=Base64.base64encode(file_contents)) - @test isa(api_return, FormsClient.TestResponse) - @test api_return.message == "success, file_id=1, metadata=my metadata, file=file contents" - @test http_resp.status == 200 - end - - return nothing -end - -function runtests(httplib::Symbol) - @testset "Forms and File Uploads" begin - test(server, httplib) - end -end - -end # module FormsV3Client \ No newline at end of file diff --git a/test/forms/forms_server.jl b/test/forms/forms_server.jl deleted file mode 100644 index ee7dba4..0000000 --- a/test/forms/forms_server.jl +++ /dev/null @@ -1,51 +0,0 @@ -module FormsV3Server - -using HTTP - -include("FormsServer/src/FormsServer.jl") - -using .FormsServer -using Base64 - -const server = Ref{Any}(nothing) - -function post_urlencoded_form(req::HTTP.Request, form_id::Int64, file::String; additional_metadata=nothing, ) - str_file_contents = file - return FormsServer.TestResponse(; message="success, form_id=$form_id, metadata=$additional_metadata, file=$str_file_contents", ) -end - -function upload_binary_file(req::HTTP.Request, file_id::Int64; additional_metadata=nothing, file=nothing,) - str_file_contents = String(copy(file)) - return FormsServer.TestResponse(; message="success, file_id=$file_id, metadata=$additional_metadata, file=$str_file_contents", ) -end - -function upload_text_file(req::HTTP.Request, file_id::Int64; additional_metadata=nothing, file=nothing,) - str_file_contents = String(copy(Base64.base64decode(file))) - return FormsServer.TestResponse(; message="success, file_id=$file_id, metadata=$additional_metadata, file=$str_file_contents", ) -end - -function stop(::HTTP.Request) - HTTP.close(server[]) - return HTTP.Response(200, "") -end - -function ping(::HTTP.Request) - return HTTP.Response(200, "") -end - -function run_server(port=8081) - try - router = HTTP.Router() - router = FormsServer.register(router, @__MODULE__) - HTTP.register!(router, "GET", "/stop", stop) - HTTP.register!(router, "GET", "/ping", ping) - server[] = HTTP.serve!(router, port) - wait(server[]) - catch ex - @error("Server error", exception=(ex, catch_backtrace())) - end -end - -end # module FormsV3Server - -FormsV3Server.run_server() \ No newline at end of file diff --git a/test/forms/generate.sh b/test/forms/generate.sh deleted file mode 100755 index 39bfd9f..0000000 --- a/test/forms/generate.sh +++ /dev/null @@ -1,10 +0,0 @@ -java -jar openapi-generator-cli.jar generate \ - -i ../specs/forms.json \ - -g julia-client \ - -o FormsClient \ - --additional-properties=packageName=FormsClient -java -jar openapi-generator-cli.jar generate \ - -i ../specs/forms.json \ - -g julia-server \ - -o FormsServer \ - --additional-properties=packageName=FormsServer diff --git a/test/modelgen/ModelGenClient/.openapi-generator-ignore b/test/modelgen/ModelGenClient/.openapi-generator-ignore deleted file mode 100644 index 7484ee5..0000000 --- a/test/modelgen/ModelGenClient/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/test/modelgen/ModelGenClient/.openapi-generator/FILES b/test/modelgen/ModelGenClient/.openapi-generator/FILES deleted file mode 100644 index fbb5e38..0000000 --- a/test/modelgen/ModelGenClient/.openapi-generator/FILES +++ /dev/null @@ -1,9 +0,0 @@ -README.md -docs/ComputeType.md -docs/DefaultApi.md -docs/TestModel.md -src/ModelGenClient.jl -src/apis/api_DefaultApi.jl -src/modelincludes.jl -src/models/model_ComputeType.jl -src/models/model_TestModel.jl diff --git a/test/modelgen/ModelGenClient/.openapi-generator/VERSION b/test/modelgen/ModelGenClient/.openapi-generator/VERSION deleted file mode 100644 index 757e674..0000000 --- a/test/modelgen/ModelGenClient/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.0.0-SNAPSHOT \ No newline at end of file diff --git a/test/modelgen/ModelGenClient/README.md b/test/modelgen/ModelGenClient/README.md deleted file mode 100644 index b37144b..0000000 --- a/test/modelgen/ModelGenClient/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# Julia API client for ModelGenClient - -Model Generation Tests - -## Overview -This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. - -- API version: 0.1.0 -- Build package: org.openapitools.codegen.languages.JuliaClientCodegen - - -## Installation -Place the Julia files generated under the `src` folder in your Julia project. Include ModelGenClient.jl in the project code. -It would include the module named ModelGenClient. - -Documentation is generated as markdown files under the `docs` folder. You can include them in your project documentation. -Documentation is also embedded in Julia which can be used with a Julia specific documentation generator. - -## API Endpoints - -Class | Method ------------- | ------------- -*DefaultApi* | [**test**](docs/DefaultApi.md#test)
**GET** /test
Test - - -## Models - - - [ComputeType](docs/ComputeType.md) - - [TestModel](docs/TestModel.md) - - - -## Authorization -Endpoints do not require authorization. - - -## Author - - - diff --git a/test/modelgen/ModelGenClient/docs/ComputeType.md b/test/modelgen/ModelGenClient/docs/ComputeType.md deleted file mode 100644 index 2fd692e..0000000 --- a/test/modelgen/ModelGenClient/docs/ComputeType.md +++ /dev/null @@ -1,11 +0,0 @@ -# ComputeType - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/modelgen/ModelGenClient/docs/DefaultApi.md b/test/modelgen/ModelGenClient/docs/DefaultApi.md deleted file mode 100644 index f25aae3..0000000 --- a/test/modelgen/ModelGenClient/docs/DefaultApi.md +++ /dev/null @@ -1,33 +0,0 @@ -# DefaultApi - -All URIs are relative to *http://localhost* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**test**](DefaultApi.md#test) | **GET** /test | Test - - -# **test** -> test(_api::DefaultApi; _mediaType=nothing) -> TestModel, OpenAPI.Clients.ApiResponse
-> test(_api::DefaultApi, response_stream::Channel; _mediaType=nothing) -> Channel{ TestModel }, OpenAPI.Clients.ApiResponse - -Test - -### Required Parameters -This endpoint does not need any parameter. - -### Return type - -[**TestModel**](TestModel.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) - diff --git a/test/modelgen/ModelGenClient/docs/SchemaIsEnum.md b/test/modelgen/ModelGenClient/docs/SchemaIsEnum.md deleted file mode 100644 index 1f1c721..0000000 --- a/test/modelgen/ModelGenClient/docs/SchemaIsEnum.md +++ /dev/null @@ -1,11 +0,0 @@ -# SchemaIsEnum - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/modelgen/ModelGenClient/docs/TestModel.md b/test/modelgen/ModelGenClient/docs/TestModel.md deleted file mode 100644 index 15062c9..0000000 --- a/test/modelgen/ModelGenClient/docs/TestModel.md +++ /dev/null @@ -1,18 +0,0 @@ -# TestModel - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**limited_by** | **String** | | [optional] [default to "time"] -**default_date** | **Date** | | [optional] [default to OpenAPI.str2date("2011-11-11")] -**default_datetime** | **ZonedDateTime** | | [optional] [default to OpenAPI.str2zoneddatetime("2011-11-11T11:11:11Z")] -**max_val** | **Int64** | | [optional] [default to 100] -**message** | **String** | | [optional] [default to "success"] -**name** | **String** | | [default to "new"] -**compute** | [***ComputeType**](ComputeType.md) | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/modelgen/ModelGenClient/src/ModelGenClient.jl b/test/modelgen/ModelGenClient/src/ModelGenClient.jl deleted file mode 100644 index f62b2ca..0000000 --- a/test/modelgen/ModelGenClient/src/ModelGenClient.jl +++ /dev/null @@ -1,16 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -module ModelGenClient - -using Dates, TimeZones -using OpenAPI -using OpenAPI.Clients - -const API_VERSION = "0.1.0" - -include("modelincludes.jl") - -include("apis/api_DefaultApi.jl") - -end # module ModelGenClient diff --git a/test/modelgen/ModelGenClient/src/apis/api_DefaultApi.jl b/test/modelgen/ModelGenClient/src/apis/api_DefaultApi.jl deleted file mode 100644 index da8dd53..0000000 --- a/test/modelgen/ModelGenClient/src/apis/api_DefaultApi.jl +++ /dev/null @@ -1,41 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -struct DefaultApi <: OpenAPI.APIClientImpl - client::OpenAPI.Clients.Client -end - -""" -The default API base path for APIs in `DefaultApi`. -This can be used to construct the `OpenAPI.Clients.Client` instance. -""" -basepath(::Type{ DefaultApi }) = "http://localhost" - -const _returntypes_test_DefaultApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => TestModel, -) - -function _oacinternal_test(_api::DefaultApi; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_test_DefaultApi, "/test", []) - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Test - -Params: - -Return: TestModel, OpenAPI.Clients.ApiResponse -""" -function test(_api::DefaultApi; _mediaType=nothing) - _ctx = _oacinternal_test(_api; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function test(_api::DefaultApi, response_stream::Channel; _mediaType=nothing) - _ctx = _oacinternal_test(_api; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -export test diff --git a/test/modelgen/ModelGenClient/src/modelincludes.jl b/test/modelgen/ModelGenClient/src/modelincludes.jl deleted file mode 100644 index 52efb35..0000000 --- a/test/modelgen/ModelGenClient/src/modelincludes.jl +++ /dev/null @@ -1,5 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -include("models/model_ComputeType.jl") -include("models/model_TestModel.jl") diff --git a/test/modelgen/ModelGenClient/src/models/model_ComputeType.jl b/test/modelgen/ModelGenClient/src/models/model_ComputeType.jl deleted file mode 100644 index 88bb311..0000000 --- a/test/modelgen/ModelGenClient/src/models/model_ComputeType.jl +++ /dev/null @@ -1,9 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -if !isdefined(@__MODULE__, :ComputeType) - const ComputeType = String -else - @warn("Skipping redefinition of ComputeType to String") -end diff --git a/test/modelgen/ModelGenClient/src/models/model_TestModel.jl b/test/modelgen/ModelGenClient/src/models/model_TestModel.jl deleted file mode 100644 index 98a99dd..0000000 --- a/test/modelgen/ModelGenClient/src/models/model_TestModel.jl +++ /dev/null @@ -1,67 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""TestModel - - TestModel(; - limited_by="time", - default_date=OpenAPI.str2date("2011-11-11"), - default_datetime=OpenAPI.str2zoneddatetime("2011-11-11T11:11:11Z"), - max_val=100, - message="success", - name="new", - compute=nothing, - ) - - - limited_by::String - - default_date::Date - - default_datetime::ZonedDateTime - - max_val::Int64 - - message::String - - name::String - - compute::ComputeType -""" -Base.@kwdef mutable struct TestModel <: OpenAPI.APIModel - limited_by::Union{Nothing, String} = "time" - default_date::Union{Nothing, Date} = OpenAPI.str2date("2011-11-11") - default_datetime::Union{Nothing, ZonedDateTime} = OpenAPI.str2zoneddatetime("2011-11-11T11:11:11Z") - max_val::Union{Nothing, Int64} = 100 - message::Union{Nothing, String} = "success" - name::Union{Nothing, String} = "new" - compute = nothing # spec type: Union{ Nothing, ComputeType } - - function TestModel(limited_by, default_date, default_datetime, max_val, message, name, compute, ) - OpenAPI.validate_property(TestModel, Symbol("limited_by"), limited_by) - OpenAPI.validate_property(TestModel, Symbol("default_date"), default_date) - OpenAPI.validate_property(TestModel, Symbol("default_datetime"), default_datetime) - OpenAPI.validate_property(TestModel, Symbol("max_val"), max_val) - OpenAPI.validate_property(TestModel, Symbol("message"), message) - OpenAPI.validate_property(TestModel, Symbol("name"), name) - OpenAPI.validate_property(TestModel, Symbol("compute"), compute) - return new(limited_by, default_date, default_datetime, max_val, message, name, compute, ) - end -end # type TestModel - -const _property_types_TestModel = Dict{Symbol,String}(Symbol("limited_by")=>"String", Symbol("default_date")=>"Date", Symbol("default_datetime")=>"ZonedDateTime", Symbol("max_val")=>"Int64", Symbol("message")=>"String", Symbol("name")=>"String", Symbol("compute")=>"ComputeType", ) -OpenAPI.property_type(::Type{ TestModel }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_TestModel[name]))} - -function check_required(o::TestModel) - o.name === nothing && (return false) - true -end - -function OpenAPI.validate_property(::Type{ TestModel }, name::Symbol, val) - if name === Symbol("limited_by") - OpenAPI.validate_param(name, "TestModel", :enum, val, ["time", "cost", "unlimited"]) - end - if name === Symbol("default_date") - OpenAPI.validate_param(name, "TestModel", :format, val, "date") - end - if name === Symbol("default_datetime") - OpenAPI.validate_param(name, "TestModel", :format, val, "date-time") - end - if name === Symbol("max_val") - OpenAPI.validate_param(name, "TestModel", :enum, val, [100, 200, 300]) - end -end diff --git a/test/modelgen/ModelGenServer/.openapi-generator-ignore b/test/modelgen/ModelGenServer/.openapi-generator-ignore deleted file mode 100644 index 7484ee5..0000000 --- a/test/modelgen/ModelGenServer/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/test/modelgen/ModelGenServer/.openapi-generator/FILES b/test/modelgen/ModelGenServer/.openapi-generator/FILES deleted file mode 100644 index e047fdf..0000000 --- a/test/modelgen/ModelGenServer/.openapi-generator/FILES +++ /dev/null @@ -1,9 +0,0 @@ -README.md -docs/ComputeType.md -docs/DefaultApi.md -docs/TestModel.md -src/ModelGenServer.jl -src/apis/api_DefaultApi.jl -src/modelincludes.jl -src/models/model_ComputeType.jl -src/models/model_TestModel.jl diff --git a/test/modelgen/ModelGenServer/.openapi-generator/VERSION b/test/modelgen/ModelGenServer/.openapi-generator/VERSION deleted file mode 100644 index 757e674..0000000 --- a/test/modelgen/ModelGenServer/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.0.0-SNAPSHOT \ No newline at end of file diff --git a/test/modelgen/ModelGenServer/README.md b/test/modelgen/ModelGenServer/README.md deleted file mode 100644 index 35e8085..0000000 --- a/test/modelgen/ModelGenServer/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Julia API server for ModelGenServer - -Model Generation Tests - -## Overview -This API server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. - -- API version: 0.1.0 -- Build package: org.openapitools.codegen.languages.JuliaServerCodegen - - -## Installation -Place the Julia files generated under the `src` folder in your Julia project. Include ModelGenServer.jl in the project code. -It would include the module named ModelGenServer. - -Implement the server methods as listed below. They are also documented with the ModelGenServer module. -Launch a HTTP server with a router that has all handlers registered. A `register` method is provided in ModelGenServer module for convenience. - -```julia -register( - router::HTTP.Router, # Router to register handlers in - impl; # Module that implements the server methods - path_prefix::String="", # Prefix to be applied to all paths - optional_middlewares... # Optional middlewares to be applied to all handlers -) -``` - -Optional middlewares can be one or more of: -- `init`: called before the request is processed -- `pre_validation`: called after the request is parsed but before validation -- `pre_invoke`: called after validation but before the handler is invoked -- `post_invoke`: called after the handler is invoked but before the response is sent - -The order in which middlewares are invoked are: -`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` - - -## API Endpoints - -The following server methods must be implemented: - -Class | Method | HTTP request | Description ------------- | ------------- | ------------- | ------------- -*DefaultApi* | [**test**](docs/DefaultApi.md#test) | **GET** /test | Test - - - -## Models - - - [ComputeType](docs/ComputeType.md) - - [TestModel](docs/TestModel.md) - - - -## Author - - - diff --git a/test/modelgen/ModelGenServer/docs/ComputeType.md b/test/modelgen/ModelGenServer/docs/ComputeType.md deleted file mode 100644 index 2fd692e..0000000 --- a/test/modelgen/ModelGenServer/docs/ComputeType.md +++ /dev/null @@ -1,11 +0,0 @@ -# ComputeType - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/modelgen/ModelGenServer/docs/DefaultApi.md b/test/modelgen/ModelGenServer/docs/DefaultApi.md deleted file mode 100644 index 32059b5..0000000 --- a/test/modelgen/ModelGenServer/docs/DefaultApi.md +++ /dev/null @@ -1,32 +0,0 @@ -# DefaultApi - -All URIs are relative to *http://localhost* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**test**](DefaultApi.md#test) | **GET** /test | Test - - -# **test** -> test(req::HTTP.Request;) -> TestModel - -Test - -### Required Parameters -This endpoint does not need any parameter. - -### Return type - -[**TestModel**](TestModel.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/test/modelgen/ModelGenServer/docs/SchemaIsEnum.md b/test/modelgen/ModelGenServer/docs/SchemaIsEnum.md deleted file mode 100644 index 1f1c721..0000000 --- a/test/modelgen/ModelGenServer/docs/SchemaIsEnum.md +++ /dev/null @@ -1,11 +0,0 @@ -# SchemaIsEnum - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/modelgen/ModelGenServer/docs/TestModel.md b/test/modelgen/ModelGenServer/docs/TestModel.md deleted file mode 100644 index 15062c9..0000000 --- a/test/modelgen/ModelGenServer/docs/TestModel.md +++ /dev/null @@ -1,18 +0,0 @@ -# TestModel - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**limited_by** | **String** | | [optional] [default to "time"] -**default_date** | **Date** | | [optional] [default to OpenAPI.str2date("2011-11-11")] -**default_datetime** | **ZonedDateTime** | | [optional] [default to OpenAPI.str2zoneddatetime("2011-11-11T11:11:11Z")] -**max_val** | **Int64** | | [optional] [default to 100] -**message** | **String** | | [optional] [default to "success"] -**name** | **String** | | [default to "new"] -**compute** | [***ComputeType**](ComputeType.md) | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/modelgen/ModelGenServer/src/ModelGenServer.jl b/test/modelgen/ModelGenServer/src/ModelGenServer.jl deleted file mode 100644 index a3ba062..0000000 --- a/test/modelgen/ModelGenServer/src/ModelGenServer.jl +++ /dev/null @@ -1,54 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw""" -Encapsulates generated server code for ModelGenServer - -The following server methods must be implemented: - -- **test** - - *invocation:* GET /test - - *signature:* test(req::HTTP.Request;) -> TestModel -""" -module ModelGenServer - -using HTTP -using URIs -using Dates -using TimeZones -using OpenAPI -using OpenAPI.Servers - -const API_VERSION = "0.1.0" - -include("modelincludes.jl") - -include("apis/api_DefaultApi.jl") - -""" -Register handlers for all APIs in this module in the supplied `Router` instance. - -Paramerets: -- `router`: Router to register handlers in -- `impl`: module that implements the server methods - -Optional parameters: -- `path_prefix`: prefix to be applied to all paths -- `optional_middlewares`: Register one or more optional middlewares to be applied to all requests. - -Optional middlewares can be one or more of: - - `init`: called before the request is processed - - `pre_validation`: called after the request is parsed but before validation - - `pre_invoke`: called after validation but before the handler is invoked - - `post_invoke`: called after the handler is invoked but before the response is sent - -The order in which middlewares are invoked are: -`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` -""" -function register(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - registerDefaultApi(router, impl; path_prefix=path_prefix, optional_middlewares...) - return router -end - -end # module ModelGenServer diff --git a/test/modelgen/ModelGenServer/src/apis/api_DefaultApi.jl b/test/modelgen/ModelGenServer/src/apis/api_DefaultApi.jl deleted file mode 100644 index 97660d9..0000000 --- a/test/modelgen/ModelGenServer/src/apis/api_DefaultApi.jl +++ /dev/null @@ -1,35 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -function test_read(handler) - function test_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function test_validate(handler) - function test_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - return handler(req) - end -end - -function test_invoke(impl; post_invoke=nothing) - function test_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.test(req::HTTP.Request;) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - - -function registerDefaultApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - HTTP.register!(router, "GET", path_prefix * "/test", OpenAPI.Servers.middleware(impl, test_read, test_validate, test_invoke; optional_middlewares...)) - return router -end diff --git a/test/modelgen/ModelGenServer/src/modelincludes.jl b/test/modelgen/ModelGenServer/src/modelincludes.jl deleted file mode 100644 index 52efb35..0000000 --- a/test/modelgen/ModelGenServer/src/modelincludes.jl +++ /dev/null @@ -1,5 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -include("models/model_ComputeType.jl") -include("models/model_TestModel.jl") diff --git a/test/modelgen/ModelGenServer/src/models/model_ComputeType.jl b/test/modelgen/ModelGenServer/src/models/model_ComputeType.jl deleted file mode 100644 index 88bb311..0000000 --- a/test/modelgen/ModelGenServer/src/models/model_ComputeType.jl +++ /dev/null @@ -1,9 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -if !isdefined(@__MODULE__, :ComputeType) - const ComputeType = String -else - @warn("Skipping redefinition of ComputeType to String") -end diff --git a/test/modelgen/ModelGenServer/src/models/model_TestModel.jl b/test/modelgen/ModelGenServer/src/models/model_TestModel.jl deleted file mode 100644 index 98a99dd..0000000 --- a/test/modelgen/ModelGenServer/src/models/model_TestModel.jl +++ /dev/null @@ -1,67 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""TestModel - - TestModel(; - limited_by="time", - default_date=OpenAPI.str2date("2011-11-11"), - default_datetime=OpenAPI.str2zoneddatetime("2011-11-11T11:11:11Z"), - max_val=100, - message="success", - name="new", - compute=nothing, - ) - - - limited_by::String - - default_date::Date - - default_datetime::ZonedDateTime - - max_val::Int64 - - message::String - - name::String - - compute::ComputeType -""" -Base.@kwdef mutable struct TestModel <: OpenAPI.APIModel - limited_by::Union{Nothing, String} = "time" - default_date::Union{Nothing, Date} = OpenAPI.str2date("2011-11-11") - default_datetime::Union{Nothing, ZonedDateTime} = OpenAPI.str2zoneddatetime("2011-11-11T11:11:11Z") - max_val::Union{Nothing, Int64} = 100 - message::Union{Nothing, String} = "success" - name::Union{Nothing, String} = "new" - compute = nothing # spec type: Union{ Nothing, ComputeType } - - function TestModel(limited_by, default_date, default_datetime, max_val, message, name, compute, ) - OpenAPI.validate_property(TestModel, Symbol("limited_by"), limited_by) - OpenAPI.validate_property(TestModel, Symbol("default_date"), default_date) - OpenAPI.validate_property(TestModel, Symbol("default_datetime"), default_datetime) - OpenAPI.validate_property(TestModel, Symbol("max_val"), max_val) - OpenAPI.validate_property(TestModel, Symbol("message"), message) - OpenAPI.validate_property(TestModel, Symbol("name"), name) - OpenAPI.validate_property(TestModel, Symbol("compute"), compute) - return new(limited_by, default_date, default_datetime, max_val, message, name, compute, ) - end -end # type TestModel - -const _property_types_TestModel = Dict{Symbol,String}(Symbol("limited_by")=>"String", Symbol("default_date")=>"Date", Symbol("default_datetime")=>"ZonedDateTime", Symbol("max_val")=>"Int64", Symbol("message")=>"String", Symbol("name")=>"String", Symbol("compute")=>"ComputeType", ) -OpenAPI.property_type(::Type{ TestModel }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_TestModel[name]))} - -function check_required(o::TestModel) - o.name === nothing && (return false) - true -end - -function OpenAPI.validate_property(::Type{ TestModel }, name::Symbol, val) - if name === Symbol("limited_by") - OpenAPI.validate_param(name, "TestModel", :enum, val, ["time", "cost", "unlimited"]) - end - if name === Symbol("default_date") - OpenAPI.validate_param(name, "TestModel", :format, val, "date") - end - if name === Symbol("default_datetime") - OpenAPI.validate_param(name, "TestModel", :format, val, "date-time") - end - if name === Symbol("max_val") - OpenAPI.validate_param(name, "TestModel", :enum, val, [100, 200, 300]) - end -end diff --git a/test/modelgen/generate.sh b/test/modelgen/generate.sh deleted file mode 100755 index 26933db..0000000 --- a/test/modelgen/generate.sh +++ /dev/null @@ -1,10 +0,0 @@ -java -jar openapi-generator-cli.jar generate \ - -i ../specs/modelgen.json \ - -g julia-client \ - -o ModelGenClient \ - --additional-properties=packageName=ModelGenClient -java -jar openapi-generator-cli.jar generate \ - -i ../specs/modelgen.json \ - -g julia-server \ - -o ModelGenServer \ - --additional-properties=packageName=ModelGenServer diff --git a/test/modelgen/testmodelgen.jl b/test/modelgen/testmodelgen.jl deleted file mode 100644 index 633bfe4..0000000 --- a/test/modelgen/testmodelgen.jl +++ /dev/null @@ -1,22 +0,0 @@ -module TestModelGen - using OpenAPI - using Test - - include("ModelGenClient/src/ModelGenClient.jl") - include("ModelGenServer/src/ModelGenServer.jl") - - function test_modelgen(testmodel) - @test testmodel.limited_by == "time" - @test testmodel.default_date == OpenAPI.str2date("2011-11-11") - @test testmodel.default_datetime == OpenAPI.str2zoneddatetime("2011-11-11T11:11:11Z") - @test testmodel.max_val == 100 - @test testmodel.compute in ["cpu", "gpu"] - @test testmodel.message == "success" - @test testmodel.name == "new" - end - - function runtests() - test_modelgen(ModelGenClient.TestModel(; compute="cpu")); - test_modelgen(ModelGenServer.TestModel(; compute="gpu")); - end -end # module TestModelGen diff --git a/test/models.jl b/test/models.jl new file mode 100644 index 0000000..99811d9 --- /dev/null +++ b/test/models.jl @@ -0,0 +1,201 @@ +@testset "adversarial schema models" begin + response(reference) = OpenAPI.obj( + "description" => "value", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj("\$ref" => reference), + ), + ), + ) + paths = OpenAPI.obj() + for name in ( + "TrueValue", + "FalseValue", + "ConstSeven", + "ForbiddenString", + "MixedEnum", + "AmbiguousOne", + "AnyChoice", + "NullableValue", + "Record", + "ClosedRecord", + "ConditionalRecord", + "NumericChoice", + ) + id = lowercase(name) + paths["/" * id] = OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => id, + "responses" => OpenAPI.obj( + "200" => response("#/components/schemas/" * name), + ), + ), + ) + end + document = minimal_openapi("3.1.1", paths) + document["components"] = OpenAPI.obj( + "schemas" => OpenAPI.obj( + "TrueValue" => true, + "FalseValue" => false, + "ConstSeven" => OpenAPI.obj("const" => 7), + "ForbiddenString" => OpenAPI.obj( + "not" => OpenAPI.obj("type" => "string"), + ), + "MixedEnum" => OpenAPI.obj( + "enum" => Any["x", 2, true, nothing], + ), + "AmbiguousOne" => OpenAPI.obj( + "oneOf" => Any[ + OpenAPI.obj("type" => "integer"), + OpenAPI.obj("type" => "number"), + ], + ), + "AnyChoice" => OpenAPI.obj( + "anyOf" => Any[ + OpenAPI.obj("type" => "integer"), + OpenAPI.obj("type" => "string"), + ], + ), + "NullableValue" => OpenAPI.obj( + "type" => ["string", "null"], + ), + "NumericChoice" => OpenAPI.obj( + "type" => ["integer", "number"], + ), + "Record" => OpenAPI.obj( + "type" => "object", + "required" => ["fixed"], + "properties" => OpenAPI.obj( + "fixed" => OpenAPI.obj("const" => "yes"), + "annotated-default" => OpenAPI.obj( + "type" => "string", + "default" => "server-side", + ), + ), + "propertyNames" => OpenAPI.obj("pattern" => "^[a-z-]+\$"), + "additionalProperties" => OpenAPI.obj("type" => "integer"), + ), + "ClosedRecord" => OpenAPI.obj( + "type" => "object", + "properties" => OpenAPI.obj( + "known" => OpenAPI.obj("type" => "string"), + ), + "additionalProperties" => false, + ), + "ConditionalRecord" => OpenAPI.obj( + "type" => "object", + "required" => ["kind"], + "properties" => OpenAPI.obj( + "kind" => OpenAPI.obj("type" => "string"), + "detail" => OpenAPI.obj("type" => "string"), + ), + "if" => OpenAPI.obj( + "properties" => OpenAPI.obj( + "kind" => OpenAPI.obj("const" => "detailed"), + ), + ), + "then" => OpenAPI.obj("required" => ["detail"]), + "additionalProperties" => false, + ), + # These names would otherwise collide with Julia bindings or each + # other after identifier normalization. + "String" => OpenAPI.obj( + "type" => "object", + "additionalProperties" => false, + ), + "module" => OpenAPI.obj("enum" => ["value"]), + "123-item" => OpenAPI.obj( + "type" => "object", + "additionalProperties" => false, + ), + "-" => OpenAPI.obj( + "type" => "object", + "additionalProperties" => false, + ), + "_" => OpenAPI.obj( + "type" => "object", + "additionalProperties" => false, + ), + "." => OpenAPI.obj( + "type" => "object", + "additionalProperties" => false, + ), + ), + ) + + plan = OpenAPI.plan(document; name = "SchemaEdgeClient") + names = Set(model.name for model in plan.models) + @test "StringModel" in names + @test "ModuleModel" in names + @test "Model123Item" in names + @test "Model" in names + @test "Model2" in names + @test "Model3" in names + @test length(names) == length(plan.models) + + source = OpenAPI.client(plan) + host = Module(:SchemaEdgeClientHost) + Base.include_string(host, source, "SchemaEdgeClient.jl") + C = Base.invokelatest(getfield, host, :SchemaEdgeClient) + call(name, args...) = Base.invokelatest(getfield(C, name), args...) + + media(name) = only(only(getfield(C, Symbol("_OP_", name)).responses).media) + @test call(:_schema_valid, media("truevalue")[3], Dict("anything" => 1)) + @test !call(:_schema_valid, media("falsevalue")[3], nothing) + @test call(:_schema_valid, media("constseven")[3], 7) + @test !call(:_schema_valid, media("constseven")[3], 8) + @test call(:_schema_valid, media("forbiddenstring")[3], 1) + @test !call(:_schema_valid, media("forbiddenstring")[3], "no") + + @test call(:_decode, C.MixedEnum, "x").value == "x" + @test call(:_decode, C.MixedEnum, nothing).value === nothing + @test_throws C.SchemaValidationError call(:_decode, C.MixedEnum, "bad") + @test_throws C.SchemaValidationError call(:_decode, C.AmbiguousOne, 1) + @test call(:_decode, C.AnyChoice, 2).value == 2 + @test call(:_decode, C.NullableValue, nothing) === nothing + @test call(:_decode, C.NullableValue, "ok") == "ok" + @test call(:_decode, C.NumericChoice, 3) === Int64(3) + @test call(:_decode, C.NumericChoice, 3.5) === 3.5 + + record = call( + :_decode, + C.Record, + Dict("fixed" => "yes", "extra" => 3), + ) + @test record.fixed == "yes" + @test record.annotated_default isa C.Absent + @test record.additional_properties == Dict("extra" => 3) + @test call(:_encode, record) == Dict("fixed" => "yes", "extra" => 3) + @test_throws C.SchemaValidationError call( + :_decode, + C.Record, + Dict("fixed" => "yes", "extra" => "wrong"), + ) + @test_throws C.SchemaValidationError call( + :_decode, + C.Record, + Dict("fixed" => "yes", "BAD" => 1), + ) + @test_throws C.SchemaValidationError call( + :_decode, + C.ClosedRecord, + Dict("unknown" => 1), + ) + @test_throws C.SchemaValidationError call( + :_decode, + C.ConditionalRecord, + Dict("kind" => "detailed"), + ) + @test call( + :_decode, + C.ConditionalRecord, + Dict("kind" => "simple"), + ).kind == "simple" + + conflicting = Base.invokelatest( + C.Record; + fixed = "yes", + additional_properties = Dict("fixed" => 1), + ) + @test_throws ArgumentError call(:_encode, conflicting) +end diff --git a/test/normalization.jl b/test/normalization.jl new file mode 100644 index 0000000..443bef4 --- /dev/null +++ b/test/normalization.jl @@ -0,0 +1,503 @@ +function minimal_openapi(version::AbstractString, paths) + return OpenAPI.obj( + "openapi" => String(version), + "info" => OpenAPI.obj("title" => "Test", "version" => "1.0.0"), + "paths" => paths, + ) +end + +@testset "OpenAPI normalization and schema compatibility" begin + @testset "source locations and duplicate keys" begin + json = """{ + "openapi": "3.1.0", + "info": {"title": "Test", "version": "1"}, + "paths": {} + }""" + loaded = OpenAPI.load(json) + openapi_location = OpenAPI.location( + loaded, + OpenAPI.Resources.JSONPointer("/openapi"), + ) + @test openapi_location.position == OpenAPI.SourcePosition(2, 3, 5) + + yaml = """openapi: 3.1.0 + info: + title: Test + version: "1" + paths: {} + """ + loaded_yaml = OpenAPI.load(yaml) + title_location = OpenAPI.location( + loaded_yaml, + OpenAPI.Resources.JSONPointer("/info/title"), + ) + @test title_location.position.line == 3 + @test title_location.position.column == 3 + + duplicate = """{ + "openapi": "3.1.0", + "info": {"title": "Test", "title": "Other", "version": "1"}, + "paths": {} + }""" + diagnostics = OpenAPI.check(duplicate) + @test length(diagnostics) == 1 + @test only(diagnostics).code === :parse_error + @test only(diagnostics).location.position.line == 3 + @test occursin("duplicate JSON object key", only(diagnostics).message) + + invalid = replace(json, "\"title\": \"Test\"" => "\"description\": \"Test\"") + issue = only(filter(diagnostic -> diagnostic.code === :spec_schema, OpenAPI.check(invalid))) + @test issue.location.position !== nothing + @test issue.location.position.line == 3 + end + + @testset "OAS 3.0 nullable schemas compile as nullable JSON Schema" begin + document = minimal_openapi( + "3.0.3", + OpenAPI.obj( + "/nullable" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "getNullable", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "nullable value", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "type" => "string", + "nullable" => true, + ), + ), + ), + ), + ), + ), + ), + ), + ) + source = OpenAPI.client(document; name = "NullableClient") + host = Module(:NullableClientHost) + Base.include_string(host, source, "NullableClient.jl") + client_module = Base.invokelatest(getfield, host, :NullableClient) + response = only(client_module._OP_getnullable.responses) + media = only(response.media) + @test Base.invokelatest(client_module._schema_valid, media[3], nothing) + @test Base.invokelatest( + client_module._decode_body, + client_module.DEFAULT_CLIENT, + Union{Nothing,String}, + "application/json", + Vector{UInt8}(codeunits("null")), + media[3], + ) === nothing + + constrained = minimal_openapi( + "3.0.3", + OpenAPI.obj( + "/direct" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "direct", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "direct nullable", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "\$ref" => "#/components/schemas/DirectNullable", + ), + ), + ), + ), + ), + ), + ), + "/constrained" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "constrained", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "enum excludes null", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "\$ref" => "#/components/schemas/Constrained", + ), + ), + ), + ), + ), + ), + ), + "/composed" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "composed", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "nullable has no direct type", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "\$ref" => "#/components/schemas/Composed", + ), + ), + ), + ), + ), + ), + ), + "/instance" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "instance", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "object enum instance", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "\$ref" => "#/components/schemas/InstanceEnum", + ), + ), + ), + ), + ), + ), + ), + "/choice" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "choice", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "nullable wraps oneOf", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "\$ref" => "#/components/schemas/PermissiveChoice", + ), + ), + ), + ), + ), + ), + ), + ), + ) + constrained["components"] = OpenAPI.obj( + "schemas" => OpenAPI.obj( + "BaseString" => OpenAPI.obj("type" => "string"), + "DirectNullable" => OpenAPI.obj( + "type" => "string", + "nullable" => true, + ), + "Constrained" => OpenAPI.obj( + "type" => "string", + "nullable" => true, + "enum" => ["present"], + ), + "Composed" => OpenAPI.obj( + "nullable" => true, + "allOf" => Any[ + OpenAPI.obj("\$ref" => "#/components/schemas/BaseString"), + ], + ), + "PermissiveChoice" => OpenAPI.obj( + "nullable" => true, + "oneOf" => Any[ + OpenAPI.obj("\$ref" => "#/components/schemas/BaseString"), + OpenAPI.obj("type" => "integer"), + ], + ), + "InstanceEnum" => OpenAPI.obj( + "type" => "object", + "nullable" => true, + "enum" => Any[ + OpenAPI.obj("type" => "string", "nullable" => true), + ], + ), + ), + ) + constrained_source = OpenAPI.client(constrained; name = "NullableRulesClient") + @test constrained_source == + OpenAPI.client(constrained; name = "NullableRulesClient") + constrained_host = Module(:NullableRulesClientHost) + Base.include_string( + constrained_host, + constrained_source, + "NullableRulesClient.jl", + ) + rules = Base.invokelatest(getfield, constrained_host, :NullableRulesClient) + descriptor(name) = only(only(getfield(rules, Symbol("_OP_", name)).responses).media)[3] + @test Base.invokelatest(rules._schema_valid, descriptor("direct"), nothing) + @test !Base.invokelatest( + rules._schema_valid, + descriptor("constrained"), + nothing, + ) + @test !Base.invokelatest(rules._schema_valid, descriptor("composed"), nothing) + instance = Dict("type" => "string", "nullable" => true) + @test Base.invokelatest(rules._schema_valid, descriptor("instance"), instance) + @test Nothing <: rules.DirectNullable + @test !(Nothing <: rules.Constrained) + @test !(Nothing <: rules.Composed) + + permissive_api = OpenAPI.normalize(constrained; strict = false) + @test :legacy_nullable_without_type in + Set(diagnostic.code for diagnostic in permissive_api.diagnostics) + permissive_source = OpenAPI.client( + permissive_api; + name = "PermissiveNullableClient", + ) + permissive_host = Module(:PermissiveNullableClientHost) + Base.include_string( + permissive_host, + permissive_source, + "PermissiveNullableClient.jl", + ) + permissive = Base.invokelatest( + getfield, + permissive_host, + :PermissiveNullableClient, + ) + composed_descriptor = only( + only(permissive._OP_composed.responses).media, + )[3] + @test Base.invokelatest( + permissive._schema_valid, + composed_descriptor, + nothing, + ) + constrained_descriptor = only( + only(permissive._OP_constrained.responses).media, + )[3] + @test !Base.invokelatest( + permissive._schema_valid, + constrained_descriptor, + nothing, + ) + @test Nothing <: fieldtype(permissive.Composed, :value) + @test !(Nothing <: fieldtype(permissive.Constrained, :value)) + @test Base.invokelatest( + permissive._decode, + permissive.Composed, + nothing, + ).value === nothing + choice_descriptor = only(only(permissive._OP_choice.responses).media)[3] + @test Base.invokelatest( + permissive._schema_valid, + choice_descriptor, + nothing, + ) + @test Nothing <: fieldtype(permissive.PermissiveChoice, :value) + @test Base.invokelatest( + permissive._decode, + permissive.PermissiveChoice, + nothing, + ).value === nothing + end + + @testset "modern schema planning" begin + document = minimal_openapi( + "3.1.1", + OpenAPI.obj( + "/directional" => OpenAPI.obj( + "post" => OpenAPI.obj( + "operationId" => "roundTrip", + "requestBody" => OpenAPI.obj( + "required" => true, + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "\$ref" => "#/components/schemas/Directional", + ), + ), + ), + ), + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "directional value", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "\$ref" => "#/components/schemas/Directional", + ), + ), + ), + ), + ), + ), + ), + ), + ) + document["components"] = OpenAPI.obj( + "schemas" => OpenAPI.obj( + "RootModel" => OpenAPI.obj( + "type" => "object", + "required" => ["base"], + "properties" => OpenAPI.obj( + "base" => OpenAPI.obj("type" => "string"), + ), + "additionalProperties" => false, + ), + "Extended" => OpenAPI.obj( + "\$ref" => "#/components/schemas/RootModel", + "type" => "object", + "required" => ["count"], + "properties" => OpenAPI.obj( + "count" => OpenAPI.obj("type" => "integer"), + ), + ), + "PatternMap" => OpenAPI.obj( + "type" => "object", + "patternProperties" => OpenAPI.obj( + "^x-" => OpenAPI.obj("type" => "integer"), + ), + "additionalProperties" => false, + ), + "VariableTuple" => OpenAPI.obj( + "type" => "array", + "prefixItems" => Any[ + OpenAPI.obj("type" => "string"), + OpenAPI.obj("type" => "integer"), + ], + "items" => false, + ), + "ExactTuple" => OpenAPI.obj( + "type" => "array", + "prefixItems" => Any[ + OpenAPI.obj("type" => "string"), + OpenAPI.obj("type" => "integer"), + ], + "items" => false, + "minItems" => 2, + ), + "RecursiveList" => OpenAPI.obj( + "type" => "array", + "items" => OpenAPI.obj( + "\$ref" => "#/components/schemas/RecursiveList", + ), + ), + "ChoiceA" => OpenAPI.obj( + "type" => "object", + "required" => ["kind", "input_secret", "output_id"], + "properties" => OpenAPI.obj( + "kind" => OpenAPI.obj("const" => "a"), + "input_secret" => OpenAPI.obj( + "type" => "string", + "writeOnly" => true, + ), + "output_id" => OpenAPI.obj( + "type" => "integer", + "readOnly" => true, + ), + ), + "additionalProperties" => false, + ), + "ChoiceB" => OpenAPI.obj( + "type" => "object", + "required" => ["kind", "input_secret", "output_id"], + "properties" => OpenAPI.obj( + "kind" => OpenAPI.obj("const" => "b"), + "input_secret" => OpenAPI.obj( + "type" => "string", + "writeOnly" => true, + ), + "output_id" => OpenAPI.obj( + "type" => "integer", + "readOnly" => true, + ), + ), + "additionalProperties" => false, + ), + "DirectionalChoice" => OpenAPI.obj( + "oneOf" => Any[ + OpenAPI.obj("\$ref" => "#/components/schemas/ChoiceA"), + OpenAPI.obj("\$ref" => "#/components/schemas/ChoiceB"), + ], + ), + "Directional" => OpenAPI.obj( + "type" => "object", + "required" => ["id", "secret", "name", "choice"], + "properties" => OpenAPI.obj( + "id" => OpenAPI.obj( + "type" => "integer", + "readOnly" => true, + ), + "secret" => OpenAPI.obj( + "type" => "string", + "writeOnly" => true, + ), + "name" => OpenAPI.obj("type" => "string"), + "choice" => OpenAPI.obj( + "\$ref" => "#/components/schemas/DirectionalChoice", + ), + ), + "additionalProperties" => false, + ), + ), + ) + source = OpenAPI.client(document; name = "ModernSchemaClient") + host = Module(:ModernSchemaClientHost) + Base.include_string(host, source, "ModernSchemaClient.jl") + client_module = Base.invokelatest(getfield, host, :ModernSchemaClient) + + @test fieldnames(client_module.Extended) == (:base, :count) + @test fieldtype(client_module.Extended, :base) === String + @test fieldtype(client_module.Extended, :count) === Int64 + @test fieldnames(client_module.PatternMap) == (:additional_properties,) + @test fieldtype(client_module.PatternMap, :additional_properties) === + Dict{String,Int64} + @test client_module.VariableTuple === Vector{Union{Int64,String}} + @test client_module.ExactTuple === Tuple{String,Int64} + @test fieldtype(client_module.RecursiveList, :value) === + Vector{client_module.RecursiveList} + recursive = Base.invokelatest( + client_module._decode, + client_module.RecursiveList, + Any[Any[]], + ) + @test recursive.value[1] isa client_module.RecursiveList + @test isempty(recursive.value[1].value) + + @test fieldnames(client_module.DirectionalInput) == (:secret, :name, :choice) + @test fieldnames(client_module.DirectionalOutput) == (:id, :name, :choice) + input_choice = Base.invokelatest( + client_module.DirectionalChoiceInput, + Base.invokelatest( + client_module.ChoiceAInput; + kind = "a", + input_secret = "choice-token", + ), + ) + input = Base.invokelatest( + client_module.DirectionalInput; + secret = "token", + name = "Ada", + choice = input_choice, + ) + encoded = Base.invokelatest(client_module._encode, input) + @test encoded == Dict( + "secret" => "token", + "name" => "Ada", + "choice" => Dict( + "kind" => "a", + "input_secret" => "choice-token", + ), + ) + response_media = only(only(client_module._OP_roundtrip.responses).media) + output = Base.invokelatest( + client_module._decode_body, + client_module.DEFAULT_CLIENT, + client_module.DirectionalOutput, + "application/json", + Vector{UInt8}( + codeunits( + "{\"id\":7,\"name\":\"Ada\",\"choice\":{\"kind\":\"a\",\"output_id\":9}}", + ), + ), + response_media[3], + ) + @test output.id == 7 + @test output.name == "Ada" + @test output.choice.value isa client_module.ChoiceAOutput + @test output.choice.value.output_id == 9 + end +end diff --git a/test/opa/OPAServer/src/apis/api_DataAPIApi.jl b/test/opa/OPAServer/src/apis/api_DataAPIApi.jl deleted file mode 100644 index 0eb8aa3..0000000 --- a/test/opa/OPAServer/src/apis/api_DataAPIApi.jl +++ /dev/null @@ -1,205 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -function delete_document_read(handler) - function delete_document_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["path"] = OpenAPI.Servers.to_param(String, path_params, "path", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function delete_document_validate(handler) - function delete_document_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - return handler(req) - end -end - -function delete_document_invoke(impl; post_invoke=nothing) - function delete_document_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.delete_document(req::HTTP.Request, openapi_params["path"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_document_read(handler) - function get_document_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["path"] = OpenAPI.Servers.to_param(String, path_params, "path", required=true, ) - query_params = HTTP.queryparams(URIs.URI(req.target)) - openapi_params["input"] = OpenAPI.Servers.to_param(Dict{String, Any}, query_params, "input", ) - openapi_params["pretty"] = OpenAPI.Servers.to_param(Bool, query_params, "pretty", ) - openapi_params["provenance"] = OpenAPI.Servers.to_param(Bool, query_params, "provenance", ) - openapi_params["explain"] = OpenAPI.Servers.to_param(String, query_params, "explain", ) - openapi_params["metrics"] = OpenAPI.Servers.to_param(Bool, query_params, "metrics", ) - openapi_params["instrument"] = OpenAPI.Servers.to_param(Bool, query_params, "instrument", ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_document_validate(handler) - function get_document_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - return handler(req) - end -end - -function get_document_invoke(impl; post_invoke=nothing) - function get_document_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_document(req::HTTP.Request, openapi_params["path"]; input=get(openapi_params, "input", nothing), pretty=get(openapi_params, "pretty", nothing), provenance=get(openapi_params, "provenance", nothing), explain=get(openapi_params, "explain", nothing), metrics=get(openapi_params, "metrics", nothing), instrument=get(openapi_params, "instrument", nothing),) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_document_with_path_read(handler) - function get_document_with_path_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["path"] = OpenAPI.Servers.to_param(String, path_params, "path", required=true, ) - query_params = HTTP.queryparams(URIs.URI(req.target)) - openapi_params["pretty"] = OpenAPI.Servers.to_param(Bool, query_params, "pretty", ) - openapi_params["provenance"] = OpenAPI.Servers.to_param(Bool, query_params, "provenance", ) - openapi_params["explain"] = OpenAPI.Servers.to_param(String, query_params, "explain", ) - openapi_params["metrics"] = OpenAPI.Servers.to_param(Bool, query_params, "metrics", ) - openapi_params["instrument"] = OpenAPI.Servers.to_param(Bool, query_params, "instrument", ) - openapi_params["request_body"] = OpenAPI.Servers.to_param_type(Dict{String, Any}, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_document_with_path_validate(handler) - function get_document_with_path_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - return handler(req) - end -end - -function get_document_with_path_invoke(impl; post_invoke=nothing) - function get_document_with_path_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_document_with_path(req::HTTP.Request, openapi_params["path"], openapi_params["request_body"]; pretty=get(openapi_params, "pretty", nothing), provenance=get(openapi_params, "provenance", nothing), explain=get(openapi_params, "explain", nothing), metrics=get(openapi_params, "metrics", nothing), instrument=get(openapi_params, "instrument", nothing),) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_document_with_web_hook_read(handler) - function get_document_with_web_hook_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["path"] = OpenAPI.Servers.to_param(String, path_params, "path", required=true, ) - query_params = HTTP.queryparams(URIs.URI(req.target)) - openapi_params["pretty"] = OpenAPI.Servers.to_param(Bool, query_params, "pretty", ) - openapi_params["request_body"] = OpenAPI.Servers.to_param_type(Dict{String, Any}, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_document_with_web_hook_validate(handler) - function get_document_with_web_hook_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - return handler(req) - end -end - -function get_document_with_web_hook_invoke(impl; post_invoke=nothing) - function get_document_with_web_hook_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_document_with_web_hook(req::HTTP.Request, openapi_params["path"], openapi_params["request_body"]; pretty=get(openapi_params, "pretty", nothing),) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function patch_document_read(handler) - function patch_document_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["path"] = OpenAPI.Servers.to_param(String, path_params, "path", required=true, ) - openapi_params["PatchesSchemaInner"] = OpenAPI.Servers.to_param_type(Vector{PatchesSchemaInner}, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function patch_document_validate(handler) - function patch_document_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - OpenAPI.validate_param("PatchesSchemaInner", "patch_document", :minItems, openapi_params["PatchesSchemaInner"], 1) - - return handler(req) - end -end - -function patch_document_invoke(impl; post_invoke=nothing) - function patch_document_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.patch_document(req::HTTP.Request, openapi_params["path"], openapi_params["PatchesSchemaInner"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function put_document_read(handler) - function put_document_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["path"] = OpenAPI.Servers.to_param(String, path_params, "path", required=true, ) - headers = Dict{String,String}(req.headers) - openapi_params["If-None-Match"] = OpenAPI.Servers.to_param(String, headers, "If-None-Match", ) - openapi_params["body"] = OpenAPI.Servers.to_param_type(Any, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function put_document_validate(handler) - function put_document_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - return handler(req) - end -end - -function put_document_invoke(impl; post_invoke=nothing) - function put_document_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.put_document(req::HTTP.Request, openapi_params["path"], openapi_params["body"]; if_none_match=get(openapi_params, "If-None-Match", nothing),) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - - -function registerDataAPIApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - HTTP.register!(router, "DELETE", path_prefix * "/v1/data/{path}", OpenAPI.Servers.middleware(impl, delete_document_read, delete_document_validate, delete_document_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/v1/data/{path}", OpenAPI.Servers.middleware(impl, get_document_read, get_document_validate, get_document_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/v1/data/{path}", OpenAPI.Servers.middleware(impl, get_document_with_path_read, get_document_with_path_validate, get_document_with_path_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/v0/data/{path}", OpenAPI.Servers.middleware(impl, get_document_with_web_hook_read, get_document_with_web_hook_validate, get_document_with_web_hook_invoke; optional_middlewares...)) - HTTP.register!(router, "PATCH", path_prefix * "/v1/data/{path}", OpenAPI.Servers.middleware(impl, patch_document_read, patch_document_validate, patch_document_invoke; optional_middlewares...)) - HTTP.register!(router, "PUT", path_prefix * "/v1/data/{path}", OpenAPI.Servers.middleware(impl, put_document_read, put_document_validate, put_document_invoke; optional_middlewares...)) - return router -end diff --git a/test/openapi_trim_workload.jl b/test/openapi_trim_workload.jl new file mode 100644 index 0000000..77605b0 --- /dev/null +++ b/test/openapi_trim_workload.jl @@ -0,0 +1,73 @@ +using OpenAPI + +include(joinpath(@__DIR__, "TrimClient.jl")) + +function checked(condition::Bool, message::String)::Nothing + condition || error(message) + return nothing +end + +function exercise_openapi_public_entrypoints()::Nothing + version = OpenAPI.DocumentVersion("3.1.0") + checked(version.major == 3, "document version major was wrong") + checked(OpenAPI.oas_family(version) === :oas31, "document family was wrong") + + parameter = OpenAPI.Param("id", :path, Int) + checked(parameter.name == "id", "parameter name was lost") + checked(parameter.required, "path parameter was not required") + + operation = OpenAPI.Operation(; + id = "getWidget", + method = :GET, + path = "/widgets/{id}", + params = [parameter], + responsetype = String, + secured = true, + ) + checked(operation.method === :GET, "operation method was lost") + checked(operation.secured, "operation security was lost") + + registry = OpenAPI.SchemaRegistry() + schema = OpenAPI.schemaof(registry, Int) + checked(schema isa AbstractDict, "primitive schema type was wrong") + return nothing +end + +function exercise_generated_client()::Nothing + client = TrimClient.Client("https://override.example.test") + checked(client.server == "https://override.example.test", "Client server was not set") + + credential = TrimClient.BearerCredential("trim-token") + checked(credential.token == "trim-token", "generated bearer credential lost token") + TrimClient.credential!(client, "BearerAuth", credential) + checked(haskey(client.credentials, "BearerAuth"), "credential! did not set auth") + TrimClient.clearcredential!(client, "BearerAuth") + checked(!haskey(client.credentials, "BearerAuth"), "clearcredential! did not clear auth") + + widget = TrimClient.Widget( + ; + id = 7, + name = "trim", + status = TrimClient.WidgetStatus("active"), + tags = ["a"], + ) + checked(widget.id == 7, "generated model constructor lost id") + checked(widget.name == "trim", "generated model constructor lost name") + checked(string(widget.status) == "active", "generated enum constructor lost value") + checked(widget.tags == ["a"], "generated model constructor lost tags") + return nothing +end + +function run_openapi_trim_workload()::Nothing + exercise_openapi_public_entrypoints() + exercise_generated_client() + return nothing +end + +function @main(args::Vector{String})::Cint + _ = args + run_openapi_trim_workload() + return 0 +end + +Base.Experimental.entrypoint(main, (Vector{String},)) diff --git a/test/param_deserialize.jl b/test/param_deserialize.jl deleted file mode 100644 index 2a9b7a6..0000000 --- a/test/param_deserialize.jl +++ /dev/null @@ -1,130 +0,0 @@ -using Test - -using OpenAPI.Servers: deep_dict_repr, get_param, to_param -using OpenAPI: deep_object_to_array, ValidationException - -@testset "Case-insensitive header param lookup" begin - # HTTP.jl 2.x canonicalizes incoming request header names (e.g. the wire header - # "api_key" arrives as "Api_key"), while 1.x preserves the sent case. Header field - # names are case-insensitive per RFC, so server param lookup must resolve them - # regardless of the HTTP.jl version. See get_param in src/server.jl. - canonicalized = Dict{String,String}("Api_key" => "secret", "Uuid_parameter" => "abc") - - @testset "get_param resolves canonicalized keys" begin - @test get_param(canonicalized, "api_key", false) == "secret" - @test get_param(canonicalized, "uuid_parameter", false) == "abc" - # required param present only under its canonicalized key must not throw - @test get_param(canonicalized, "api_key", true) == "secret" - end - - @testset "exact match still wins" begin - # an exact key takes precedence over any case-insensitive fallback - mixed = Dict{String,String}("api_key" => "exact", "Api_key" => "canon") - @test get_param(mixed, "api_key", false) == "exact" - end - - @testset "genuinely missing param" begin - @test get_param(canonicalized, "missing", false) === nothing - @test_throws ValidationException get_param(canonicalized, "missing", true) - end - - @testset "to_param end-to-end (as generated code calls it)" begin - @test to_param(String, canonicalized, "api_key") == "secret" - @test to_param(String, canonicalized, "uuid_parameter"; required=true) == "abc" - end -end -@testset "Test deep_dict_repr" begin - @testset "Single level object" begin - query_string = Dict("key1" => "value1", "key2" => "value2") - expected = Dict("key1" => "value1", "key2" => "value2") - @test deep_dict_repr(query_string) == expected - end - - @testset "Nested object" begin - query_string = Dict("outer[inner]" => "value") - expected = Dict("outer" => Dict("inner" => "value")) - @test deep_dict_repr(query_string) == expected - end - @testset "Deeply nested object" begin - query_string = Dict("a[b][c][d]" => "value") - expected = Dict("a" => Dict("b" => Dict("c" => Dict("d" => "value")))) - @test deep_dict_repr(query_string) == expected - end - - @testset "Multiple nested objects" begin - query_string = Dict("a[b]" => "value1", "a[c]" => "value2") - expected = Dict("a" => Dict("b" => "value1", "c" => "value2")) - @test deep_dict_repr(query_string) == expected - end - - @testset "List of values" begin - query_string = Dict("a[0]" => "value1", "a[1]" => "value2") - expected = Dict("a" => Dict("0" => "value1", "1" => "value2")) - @test deep_dict_repr(query_string) == expected - end - - @testset "Mixed structure" begin - query_string = - Dict("a[b]" => "value1", "a[c][0]" => "value2", "a[c][1]" => "value3") - expected = Dict( - "a" => Dict("b" => "value1", "c" => Dict("0" => "value2", "1" => "value3")), - ) - @test deep_dict_repr(query_string) == expected - end - - @testset "deep_object_to_array" begin - example = Dict( - "a" => Dict("b" => "value1", "c" => Dict("0" => "value2", "1" => "value3")), - ) - @test deep_object_to_array(example) == example - @test deep_object_to_array(example["a"]["c"]) == ["value2", "value3"] - end - - @testset "Blank values" begin - query_string = Dict("a[b]" => "", "a[c]" => "") - expected = Dict("a" => Dict("b" => "", "c" => "")) - @test deep_dict_repr(query_string) == expected - end - - @testset "Complex nested structure" begin - query_string = - Dict("a[b][c][d]" => "value1", "a[b][c][e]" => "value2", "a[f]" => "value3") - expected = Dict( - "a" => Dict( - "b" => Dict("c" => Dict("d" => "value1", "e" => "value2")), - "f" => "value3", - ), - ) - @test deep_dict_repr(query_string) == expected - end - @testset "Complex nested structure with numbers and nessted" begin - query_string = Dict{String,String}( - "filter[0][name]" => "name", - "filter[0][data][0]" => "Dog", - "pagination[type]" => "offset", - "pagination[page]" => "1", - "filter[0][type]" => "FilterSet", - "pagination[per_page]" => "5", - "pagination[foo]" => "5.0", - ) - expected = Dict( - "pagination" => Dict( - "page" => "1", - "per_page" => "5", - "type" => "offset", - "foo" => "5.0", - ), - "filter" => Dict( - "0" => Dict( - "name" => "name", - "data" => Dict("0" => "Dog"), - "type" => "FilterSet", - ), - ), - ) - d = deep_dict_repr(query_string) - @test d["pagination"] == expected["pagination"] - @test d["filter"] == expected["filter"] - end - -end diff --git a/test/references.jl b/test/references.jl new file mode 100644 index 0000000..69a0c07 --- /dev/null +++ b/test/references.jl @@ -0,0 +1,555 @@ +@testset "OpenAPI reference resolution" begin + @testset "relative files and cross-file schemas" begin + directory = mktempdir() + common = OpenAPI.obj( + "\$defs" => OpenAPI.obj( + "User" => OpenAPI.obj( + "type" => "object", + "required" => ["id", "name"], + "properties" => OpenAPI.obj( + "id" => OpenAPI.obj("type" => "integer"), + "name" => OpenAPI.obj("type" => "string"), + ), + "additionalProperties" => false, + ), + ), + "parameters" => OpenAPI.obj( + "Trace" => OpenAPI.obj( + "name" => "X-Trace", + "in" => "header", + "required" => false, + "schema" => OpenAPI.obj("type" => "string"), + ), + ), + ) + common_path = joinpath(directory, "common.json") + write(common_path, JSON.json(common)) + root_path = joinpath(directory, "openapi.yaml") + write( + root_path, + """ + openapi: 3.1.1 + info: + title: External + version: "1" + paths: + /users/{id}: + get: + operationId: getUser + parameters: + - name: id + in: path + required: true + schema: + type: integer + - \$ref: "./common.json#/parameters/Trace" + responses: + "200": + description: user + content: + application/json: + schema: + \$ref: "./common.json#/\$defs/User" + components: + schemas: + User: + \$ref: "./common.json#/\$defs/User" + """, + ) + + api = OpenAPI.normalize(root_path) + @test length(api.operations) == 1 + operation = only(api.operations) + @test operation.parameters[2].name == "X-Trace" + @test operation.parameters[2].required === false + @test length(api.registry) == 2 + + source = OpenAPI.client(api; name = "ExternalClient") + host = Module(:ExternalClientHost) + Base.include_string(host, source, "ExternalClient.jl") + C = Base.invokelatest(getfield, host, :ExternalClient) + @test fieldnames(C.User) == (:id, :name) + @test fieldtype(C.User, :id) === Int64 + @test fieldtype(C.User, :name) === String + + @test_throws OpenAPI.OpenAPIError OpenAPI.normalize( + root_path; + max_resources = 1, + ) + end + + @testset "OAS 3.0 nullable schemas in external component wrappers" begin + directory = mktempdir() + write( + joinpath(directory, "common.yaml"), + """ + components: + schemas: + Name: + type: string + nullable: true + LegacyChoice: + nullable: true + oneOf: + - type: integer + - type: string + """, + ) + root = minimal_openapi( + "3.0.3", + OpenAPI.obj( + "/name" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "getExternalName", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "name", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "\$ref" => "./common.yaml#/components/schemas/Name", + ), + ), + ), + ), + ), + ), + ), + "/legacy" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "getExternalLegacy", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "legacy choice", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "\$ref" => "./common.yaml#/components/schemas/LegacyChoice", + ), + ), + ), + ), + ), + ), + ), + ), + ) + root_path = joinpath(directory, "openapi.json") + write(root_path, JSON.json(root)) + + strict_source = OpenAPI.client(root_path; name = "ExternalNullableStrict") + strict_host = Module(:ExternalNullableStrictHost) + Base.include_string(strict_host, strict_source, "ExternalNullableStrict.jl") + Strict = Base.invokelatest(getfield, strict_host, :ExternalNullableStrict) + name_media = only(only(Strict._OP_getexternalname.responses).media) + legacy_media = only(only(Strict._OP_getexternallegacy.responses).media) + @test name_media[2] == Union{Nothing,String} + @test Base.invokelatest(Strict._schema_valid, name_media[3], nothing) + @test !Base.invokelatest(Strict._schema_valid, legacy_media[3], nothing) + + permissive = OpenAPI.normalize(root_path; strict = false) + @test any( + diagnostic -> diagnostic.code === :legacy_nullable_without_type, + permissive.diagnostics, + ) + permissive_source = OpenAPI.client( + permissive; + name = "ExternalNullablePermissive", + ) + permissive_host = Module(:ExternalNullablePermissiveHost) + Base.include_string( + permissive_host, + permissive_source, + "ExternalNullablePermissive.jl", + ) + Permissive = Base.invokelatest( + getfield, + permissive_host, + :ExternalNullablePermissive, + ) + permissive_media = only( + only(Permissive._OP_getexternallegacy.responses).media, + ) + @test Nothing <: permissive_media[2] + @test Base.invokelatest( + Permissive._schema_valid, + permissive_media[3], + nothing, + ) + end + + @testset "file sandbox" begin + parent = mktempdir() + source_directory = joinpath(parent, "source") + mkdir(source_directory) + external_path = joinpath(parent, "outside.json") + write( + external_path, + JSON.json( + OpenAPI.obj( + "type" => "object", + "properties" => OpenAPI.obj( + "value" => OpenAPI.obj("type" => "string"), + ), + ), + ), + ) + root = OpenAPI.obj( + "openapi" => "3.1.1", + "info" => OpenAPI.obj("title" => "Sandbox", "version" => "1"), + "paths" => OpenAPI.obj( + "/x" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "getX", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "value", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "\$ref" => "../outside.json", + ), + ), + ), + ), + ), + ), + ), + ), + ) + root_path = joinpath(source_directory, "openapi.json") + write(root_path, JSON.json(root)) + @test_throws OpenAPI.OpenAPIError OpenAPI.normalize(root_path) + @test OpenAPI.normalize(root_path; file_roots = [parent]) isa + OpenAPI.NormalizedAPI + end + + @testset "portable generated schema identity" begin + document = minimal_openapi( + "3.1.1", + OpenAPI.obj( + "/value" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "getValue", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "value", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "\$ref" => "./common.yaml#/\$defs/Value", + ), + ), + ), + ), + ), + ), + ), + ), + ) + common = OpenAPI.obj( + "\$defs" => OpenAPI.obj( + "Value" => OpenAPI.obj( + "type" => "object", + "required" => ["id"], + "properties" => OpenAPI.obj( + "id" => OpenAPI.obj("type" => "integer"), + ), + "additionalProperties" => false, + ), + ), + ) + directories = [mktempdir(), mktempdir()] + sources = String[] + for directory in directories + # JSON is valid YAML. The .yaml suffix forces the schema retriever + # through its YAML-to-JSON adapter without adding fixture noise. + write(joinpath(directory, "common.yaml"), JSON.json(common)) + path = joinpath(directory, "openapi.json") + write(path, JSON.json(document)) + push!(sources, OpenAPI.client(path; name = "PortableClient")) + end + @test sources[1] == sources[2] + @test all(directory -> !occursin(directory, sources[1]), directories) + + portable_document = deepcopy(document) + portable_document["paths"]["/value"]["get"]["responses"]["200"]["content"]["application/json"]["schema"]["\$ref"] = + "https://example.com/common.json#/\$defs/Value" + encoded = JSON.json(portable_document) + credentialed = OpenAPI.client( + encoded; + name = "PortableClient", + base_uri = "https://user:pass@example.com/openapi.json?token=secret", + allow_remote_refs = true, + retriever = OpenAPI.Resources.MemoryRetriever( + Dict( + OpenAPI.Resources.ResourceId("https://example.com/common.json") => + JSON.json(common), + ), + ), + ) + clean = OpenAPI.client( + encoded; + name = "PortableClient", + base_uri = "https://example.com/openapi.json", + allow_remote_refs = true, + retriever = OpenAPI.Resources.MemoryRetriever( + Dict( + OpenAPI.Resources.ResourceId("https://example.com/common.json") => + JSON.json(common), + ), + ), + ) + @test credentialed == clean + @test !occursin("user:pass", credentialed) + @test !occursin("token=secret", credentialed) + end + + @testset "HTTP origin policy and redirects" begin + external_schema = JSON.json( + OpenAPI.obj( + "\$defs" => OpenAPI.obj( + "User" => OpenAPI.obj( + "type" => "object", + "required" => ["id"], + "properties" => OpenAPI.obj( + "id" => OpenAPI.obj("type" => "integer"), + ), + "additionalProperties" => false, + ), + ), + ), + ) + referenced_document(reference) = JSON.json( + OpenAPI.obj( + "openapi" => "3.1.1", + "info" => OpenAPI.obj( + "title" => "HTTP references", + "version" => "1", + ), + "paths" => OpenAPI.obj( + "/users" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "getUsers", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "users", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "type" => "array", + "items" => OpenAPI.obj( + "\$ref" => reference, + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ) + + cross_schema_requests = Ref(0) + same_schema_requests = Ref(0) + cross_server = HTTP.serve!( + function (request) + if String(request.target) == "/schema.json" + cross_schema_requests[] += 1 + return HTTP.Response( + 200, + ["Content-Type" => "application/schema+json"], + external_schema, + ) + end + return HTTP.Response(404) + end, + "127.0.0.1", + 0; + verbose = false, + ) + same_base = Ref("") + cross_base = "http://127.0.0.1:$(HTTP.port(cross_server))" + same_server = HTTP.serve!( + function (request) + target = String(request.target) + if target == "/schema.json" + same_schema_requests[] += 1 + return HTTP.Response( + 200, + ["Content-Type" => "application/schema+json"], + external_schema, + ) + end + target == "/same.json" && return HTTP.Response( + 200, + ["Content-Type" => "application/openapi+json"], + referenced_document("/schema.json#/\$defs/User"), + ) + target == "/cross.json" && return HTTP.Response( + 200, + ["Content-Type" => "application/openapi+json"], + referenced_document( + cross_base * "/schema.json#/\$defs/User", + ), + ) + target == "/redirect.json" && return HTTP.Response( + 302, + ["Location" => same_base[] * "/same.json"], + ) + target == "/oversized.json" && return HTTP.Response( + 200, + ["Content-Type" => "application/openapi+json"], + repeat("x", 4096), + ) + return HTTP.Response(404) + end, + "127.0.0.1", + 0; + verbose = false, + ) + same_base[] = "http://127.0.0.1:$(HTTP.port(same_server))" + try + same = OpenAPI.normalize(same_base[] * "/same.json") + same_plan = OpenAPI.plan(same) + @test same_schema_requests[] > 0 + @test startswith(only(same_plan.operations).return_type, "Vector{") + @test any( + field -> field.wire_name == "id" && field.type == "Int64", + only(same_plan.models).fields, + ) + + blocked = @test_throws OpenAPI.OpenAPIError OpenAPI.normalize( + same_base[] * "/cross.json", + ) + @test any( + diagnostic -> diagnostic.code === :invalid_schema, + blocked.value.diagnostics, + ) + @test cross_schema_requests[] == 0 + allowed = OpenAPI.normalize( + same_base[] * "/cross.json"; + allow_remote_refs = true, + ) + @test cross_schema_requests[] > 0 + @test startswith( + only(OpenAPI.plan(allowed).operations).return_type, + "Vector{", + ) + + @test_throws OpenAPI.Resources.RetrievalError OpenAPI.load( + same_base[] * "/redirect.json", + ) + oversized = @test_throws OpenAPI.Resources.RetrievalError OpenAPI.load( + same_base[] * "/oversized.json"; + max_bytes = 128, + ) + @test occursin("128-byte limit", sprint(showerror, oversized.value)) + finally + close(same_server) + close(cross_server) + end + end + + @testset "non-schema cycles and Path Item siblings" begin + cycle = OpenAPI.obj( + "openapi" => "3.1.1", + "info" => OpenAPI.obj("title" => "Cycle", "version" => "1"), + "paths" => OpenAPI.obj( + "/x" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "cycle", + "parameters" => Any[ + OpenAPI.obj( + "\$ref" => "#/components/parameters/A", + ), + ], + "responses" => OpenAPI.obj( + "204" => OpenAPI.obj("description" => "empty"), + ), + ), + ), + ), + "components" => OpenAPI.obj( + "parameters" => OpenAPI.obj( + "A" => OpenAPI.obj( + "\$ref" => "#/components/parameters/B", + ), + "B" => OpenAPI.obj( + "\$ref" => "#/components/parameters/A", + ), + ), + ), + ) + error = @test_throws OpenAPI.OpenAPIError OpenAPI.normalize(cycle) + @test any( + diagnostic -> diagnostic.code === :reference_cycle, + error.value.diagnostics, + ) + + siblings = OpenAPI.obj( + "openapi" => "3.1.1", + "info" => OpenAPI.obj("title" => "Path refs", "version" => "1"), + "paths" => OpenAPI.obj( + "/x" => OpenAPI.obj( + "\$ref" => "#/components/pathItems/Base", + "get" => OpenAPI.obj( + "operationId" => "local", + "responses" => OpenAPI.obj( + "204" => OpenAPI.obj("description" => "local"), + ), + ), + ), + ), + "components" => OpenAPI.obj( + "pathItems" => OpenAPI.obj( + "Base" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "base", + "responses" => OpenAPI.obj( + "204" => OpenAPI.obj("description" => "base"), + ), + ), + ), + ), + ), + ) + @test_throws OpenAPI.OpenAPIError OpenAPI.normalize(siblings) + permissive = OpenAPI.normalize(siblings; strict = false) + @test only(permissive.operations).id == "local" + @test any( + diagnostic -> diagnostic.code === :path_item_reference_siblings && + diagnostic.severity === :warning, + permissive.diagnostics, + ) + end + + @testset "input resource limits" begin + document = OpenAPI.obj( + "openapi" => "3.1.1", + "info" => OpenAPI.obj("title" => "Limits", "version" => "1"), + "paths" => OpenAPI.obj(), + "x-deep" => OpenAPI.obj( + "one" => OpenAPI.obj( + "two" => OpenAPI.obj( + "three" => OpenAPI.obj("four" => true), + ), + ), + ), + ) + @test_throws OpenAPI.OpenAPIError OpenAPI.load( + JSON.json(document); + max_depth = 3, + ) + @test_throws OpenAPI.OpenAPIError OpenAPI.load( + JSON.json(document); + max_nodes = 5, + ) + @test_throws ArgumentError OpenAPI.load( + JSON.json(document); + max_bytes = 8, + ) + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 6ab75ad..7746c52 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,227 +1,260 @@ -using Test, HTTP +using Test, OpenAPI, JSON, Dates +import Downloads, HTTP -import OpenAPI +const SchemaEngine = OpenAPI.SchemaEngine -include("chunkreader_tests.jl") -include("streaming_latency_tests.jl") -include("testutils.jl") -include("modelgen/testmodelgen.jl") -include("client/runtests.jl") -include("client/allany/runtests.jl") -include("forms/forms_client.jl") -include("client/timeouttest/runtests.jl") -include("deep_object/deep_client.jl") +# ── domain types used across the tests ────────────────────────────────────── + +struct TWidget + id::Int + tags::Vector{String} +end + +struct TOrder + widget::TWidget + count::Int + note::Union{Nothing,String} + placed::Date +end + +@enum TColor tred tgreen tblue + +struct TNode + value::Int + next::Union{Nothing,TNode} +end + +getschema(reg, T) = OpenAPI.schemaof(reg, T) @testset "OpenAPI" begin - include("param_deserialize.jl") - @testset "ModelGen" begin - TestModelGen.runtests() - end - @testset "Chunk Readers" begin - ChunkReaderTests.runtests() - end - @testset "Streaming Latency" begin - StreamingLatencyTests.runtests() - end - @testset "Petstore Client" begin - try - if run_tests_with_servers && !openapi_generator_env - run(`bash client/petstore_v2/start_petstore_server.sh`) - run(`bash client/petstore_v3/start_petstore_server.sh`) - sleep(20) # let servers start - end - for httplib in values(OpenAPI.Clients.HTTPLib) - OpenAPIClientTests.runtests(httplib; skip_petstore=openapi_generator_env, test_file_upload=false) - end - finally - if run_tests_with_servers && !openapi_generator_env - run(`bash client/petstore_v2/stop_petstore_server.sh`) - run(`bash client/petstore_v3/stop_petstore_server.sh`) - end - end - end - run_tests_with_servers && !openapi_generator_env && sleep(20) # avoid port conflicts - @testset "Petstore Server" begin - v2_ret = v2_out = v3_ret = v3_out = nothing - servers_running = true - - try - if run_tests_with_servers - v2_ret, v2_out = run_server(joinpath(@__DIR__, "server", "petstore_v2", "petstore_server.jl")) - v3_ret, v3_out = run_server(joinpath(@__DIR__, "server", "petstore_v3", "petstore_server.jl")) - servers_running &= wait_server(8080) - servers_running &= wait_server(8081) - else - servers_running = false - end - for httplib in values(OpenAPI.Clients.HTTPLib) - servers_running && OpenAPIClientTests.runtests(httplib; test_file_upload=true) - end - finally - if run_tests_with_servers && !servers_running - # we probably had an error starting the servers - v2_out_str = isnothing(v2_out) ? "" : String(take!(v2_out)) - v3_out_str = isnothing(v3_out) ? "" : String(take!(v3_out)) - @warn("Servers not running", v2_ret=v2_ret, v2_out_str, v3_ret=v3_ret, v3_out_str) - end - if run_tests_with_servers && servers_running - stop_server(8080, v2_ret, v2_out) - stop_server(8081, v3_ret, v3_out) - end - end - end - run_tests_with_servers && sleep(20) # avoid port conflicts - @testset "Petstore Server (openapi-generator)" begin - v3_ret = v3_out = nothing - servers_running = true - - try - if run_tests_with_servers - v3_ret, v3_out = run_server(joinpath(@__DIR__, "server", "openapigenerator_petstore_v3", "petstore_server.jl")) - servers_running &= wait_server(8081) - else - servers_running = false - end - for httplib in values(OpenAPI.Clients.HTTPLib) - servers_running && OpenAPIClientTests.run_openapigenerator_tests(httplib; test_file_upload=true) - end - finally - if run_tests_with_servers && !servers_running - # we probably had an error starting the servers - v3_out_str = isnothing(v3_out) ? "" : String(take!(v3_out)) - @warn("Servers not running", v3_ret=v3_ret, v3_out_str) - end - if run_tests_with_servers && servers_running - stop_server(8081, v3_ret, v3_out) - end - end - end - run_tests_with_servers && sleep(20) # avoid port conflicts - @testset "Forms and File Uploads" begin - ret = out = nothing - servers_running = true - - try - if run_tests_with_servers - ret, out = run_server(joinpath(@__DIR__, "forms", "forms_server.jl")) - servers_running &= wait_server(8081) - for httplib in values(OpenAPI.Clients.HTTPLib) - FormsV3Client.runtests(httplib) - end - else - servers_running = false - end - finally - if run_tests_with_servers && !servers_running - # we probably had an error starting the servers - out_str = isnothing(out) ? "" : String(take!(out)) - @warn("Servers not running", ret=ret, out_str) - end - run_tests_with_servers && servers_running && stop_server(8081, ret, out) - end - end - run_tests_with_servers && sleep(20) # avoid port conflicts - @testset "DeepObject tests" begin - ret = out = nothing - servers_running = true - try - if run_tests_with_servers - ret, out = run_server(joinpath(@__DIR__, "deep_object", "deep_server.jl")) - servers_running &= wait_server(8081) - for httplib in values(OpenAPI.Clients.HTTPLib) - DeepClientTest.runtests(httplib) - end - else - servers_running = false - end - finally - if run_tests_with_servers && !servers_running - # we probably had an error starting the servers - out_str = isnothing(out) ? "" : String(take!(out)) - @warn("Servers not running", ret=ret, out_str) - end - run_tests_with_servers && servers_running && stop_server(8081, ret, out) - end - end - run_tests_with_servers && sleep(20) # avoid port conflicts - @testset "Union types" begin - ret = out = nothing - servers_running = true - - try - if run_tests_with_servers - ret, out = run_server(joinpath(@__DIR__, "server", "allany", "allany_server.jl")) - servers_running &= wait_server(8081) - for httplib in values(OpenAPI.Clients.HTTPLib) - AllAnyTests.runtests(httplib) - end - else - servers_running = false - end - finally - if run_tests_with_servers && !servers_running - # we probably had an error starting the servers - out_str = isnothing(out) ? "" : String(take!(out)) - @warn("Servers not running", ret=ret, out_str) - end - run_tests_with_servers && stop_server(8081, ret, out) - end + + @testset "Julia types -> JSON Schema" begin + reg = OpenAPI.SchemaRegistry() + @test getschema(reg, Int)["type"] == "integer" + @test getschema(reg, Int)["format"] == "int64" + @test getschema(reg, Float64) == + OpenAPI.obj("type" => "number", "format" => "double") + @test getschema(reg, Bool)["type"] == "boolean" + @test getschema(reg, String)["type"] == "string" + @test getschema(reg, Symbol)["type"] == "string" + @test getschema(reg, Date) == OpenAPI.obj("type" => "string", "format" => "date") + @test getschema(reg, DateTime)["format"] == "date-time" + @test getschema(reg, Any) == OpenAPI.obj() + @test getschema(reg, Vector{Int}) == OpenAPI.obj( + "type" => "array", + "items" => OpenAPI.obj("type" => "integer", "format" => "int64"), + ) + @test getschema(reg, Dict{String,Bool})["additionalProperties"]["type"] == "boolean" + @test getschema(reg, TColor)["enum"] == ["tred", "tgreen", "tblue"] + + nullable = getschema(reg, Union{Nothing,Int}) + @test haskey(nullable, "oneOf") + @test OpenAPI.obj("type" => "null") in nullable["oneOf"] + + # named tuples become inline object schemas + nt = getschema(reg, typeof((; ok = true, n = 1))) + @test nt["type"] == "object" + @test nt["required"] == ["ok", "n"] + + # structs register once and are referenced + @test getschema(reg, TWidget) == + OpenAPI.obj("\$ref" => "#/components/schemas/TWidget") + @test getschema(reg, TWidget) == + OpenAPI.obj("\$ref" => "#/components/schemas/TWidget") + tw = reg.schemas["TWidget"] + @test tw["properties"]["id"]["type"] == "integer" + @test tw["properties"]["tags"]["type"] == "array" + @test tw["required"] == ["id", "tags"] + + # nested refs, optional (Union{Nothing}) fields, date formats + getschema(reg, TOrder) + to = reg.schemas["TOrder"] + @test to["properties"]["widget"]["\$ref"] == "#/components/schemas/TWidget" + @test to["properties"]["placed"]["format"] == "date" + @test to["required"] == ["widget", "count", "placed"] # note is optional + + # self-referential types terminate + getschema(reg, TNode) + @test haskey(reg.schemas, "TNode") end - run_tests_with_servers && sleep(20) # avoid port conflicts - @testset "Debug and Verbose" begin - ret = out = nothing - servers_running = true - - try - if run_tests_with_servers - ret, out = run_server(joinpath(@__DIR__, "server", "allany", "allany_server.jl")) - servers_running &= wait_server(8081) - if VERSION >= v"1.7" - for httplib in values(OpenAPI.Clients.HTTPLib) - AllAnyTests.test_debug(httplib) - end - end - else - servers_running = false - end - finally - if run_tests_with_servers && !servers_running - # we probably had an error starting the servers - out_str = isnothing(out) ? "" : String(take!(out)) - @warn("Servers not running", ret=ret, out_str) - end - run_tests_with_servers && servers_running && stop_server(8081, ret, out) - end + + ops = [ + OpenAPI.Operation(; + id = "getwidget", + method = :GET, + path = "/w/{id}", + summary = "Get a widget", + params = [ + OpenAPI.Param("id", :path, Int), + OpenAPI.Param("owner", :query, String), + OpenAPI.Param("verbose", :query, Bool; required = false), + ], + responsetype = TWidget, + ), + OpenAPI.Operation(; + id = "neworder", + method = :POST, + path = "/orders", + bodytype = TOrder, + responsetype = TOrder, + secured = true, + ), + OpenAPI.Operation(; + id = "rmorder", + method = :DELETE, + path = "/orders/{id}", + params = [OpenAPI.Param("id", :path, Int)], + responsetype = Nothing, + ), + OpenAPI.Operation(; + id = "getwidget", + method = :GET, + path = "/w2/{id}", + params = [OpenAPI.Param("id", :path, Int)], + ), + ] + doc = OpenAPI.document( + ops; + title = "Test API", + version = "1.2.3", + description = "a test api", + servers = ["http://api.example"], + ) + + @testset "document generation" begin + @test doc["openapi"] == "3.2.0" + @test doc["info"]["title"] == "Test API" + @test doc["info"]["version"] == "1.2.3" + @test doc["servers"][1]["url"] == "http://api.example" + + get1 = doc["paths"]["/w/{id}"]["get"] + @test get1["operationId"] == "getwidget" + @test get1["summary"] == "Get a widget" + p1, p2, p3 = get1["parameters"] + @test p1["name"] == "id" && p1["in"] == "path" && p1["required"] === true + @test p2["name"] == "owner" && p2["in"] == "query" && p2["required"] === true + @test p3["name"] == "verbose" && p3["required"] === false + @test get1["responses"]["200"]["content"]["application/json"]["schema"]["\$ref"] == + "#/components/schemas/TWidget" + @test haskey(get1["responses"], "default") + + post = doc["paths"]["/orders"]["post"] + @test post["requestBody"]["required"] === true + @test post["requestBody"]["content"]["application/json"]["schema"]["\$ref"] == + "#/components/schemas/TOrder" + @test post["security"] == [OpenAPI.obj("bearerAuth" => String[])] + + del = doc["paths"]["/orders/{id}"]["delete"] + @test haskey(del["responses"], "204") + @test !haskey(del["responses"], "200") + + # duplicate operationIds are deduped + @test doc["paths"]["/w2/{id}"]["get"]["operationId"] == "getwidget_2" + + @test haskey(doc["components"]["schemas"], "TWidget") + @test haskey(doc["components"]["schemas"], "TOrder") + @test doc["components"]["securitySchemes"]["bearerAuth"]["scheme"] == "bearer" + + @test OpenAPI.validate(doc) === doc + + # invalid operations fail at construction + @test_throws ArgumentError OpenAPI.Operation(; + id = "x", + method = :FETCH, + path = "/x", + ) + @test_throws ArgumentError OpenAPI.Operation(; + id = "x", + method = :GET, + path = "nope", + ) + @test_throws ArgumentError OpenAPI.Operation(; + id = "x", + method = :GET, + path = "/a/{b}", + ) + @test_throws ArgumentError OpenAPI.Operation(; + id = "x", + method = :GET, + path = "/a", + params = [OpenAPI.Param("b", :path, Int)], + ) + @test_throws ArgumentError OpenAPI.Param("x", :header) + @test_throws ArgumentError OpenAPI.Param("x", :path; required = false) end - @testset "Helper Methods" begin - AllAnyTests.test_http_resp() + @testset "read & validate" begin + # JSON string round trip + doc2 = OpenAPI.read(JSON.json(doc)) + @test doc2["info"]["title"] == "Test API" + @test doc2["paths"]["/w/{id}"]["get"]["operationId"] == "getwidget" + + # file round trip + dir = mktempdir() + file = joinpath(dir, "api.json") + write(file, JSON.json(doc; pretty = 2)) + @test OpenAPI.read(file)["info"]["version"] == "1.2.3" + + @test_throws ArgumentError OpenAPI.read("""{"openapi": "2.0", "info": {}}""") + @test_throws ArgumentError OpenAPI.read("""{"openapi": "3.1.0"}""") + @test_throws ArgumentError OpenAPI.read("definitely not a document") + response_optional = OpenAPI.obj( + "openapi" => "3.2.0", + "info" => OpenAPI.obj("title" => "t", "version" => "1"), + "paths" => OpenAPI.obj("/x" => OpenAPI.obj("get" => OpenAPI.obj())), + ) + @test OpenAPI.validate(response_optional) === response_optional + error = @test_throws ArgumentError OpenAPI.validate( + OpenAPI.obj( + "openapi" => "3.1.0", + "info" => OpenAPI.obj("title" => "t", "version" => "1"), + "paths" => OpenAPI.obj("/x" => OpenAPI.obj("get" => OpenAPI.obj())), + ), + ) + @test occursin("responses", sprint(showerror, error.value)) end - @testset "Timeout Handling" begin - ret = out = nothing - servers_running = true - - try - if run_tests_with_servers - ret, out = run_server(joinpath(@__DIR__, "server", "timeouttest", "timeouttest_server.jl")) - servers_running &= wait_server(8081) - for httplib in values(OpenAPI.Clients.HTTPLib) - TimeoutTests.runtests(httplib) - end - else - servers_running = false - end - finally - if run_tests_with_servers && !servers_running - # we probably had an error starting the servers - out_str = isnothing(out) ? "" : String(take!(out)) - @warn("Servers not running", ret=ret, out_str) - end - run_tests_with_servers && stop_server(8081, ret, out) + @testset "client generation (static)" begin + src = OpenAPI.client(doc; name = "TestClient") + @test occursin("module TestClient", src) + host = Module(:TestClientHost) + Base.include_string(host, src, "TestClient.jl") + C = Base.invokelatest(getfield, host, :TestClient) + + @test C.SERVER[] == "http://api.example" + for f in (:getwidget, :neworder, :rmorder, :getwidget_2, :server!, :authorization!) + @test isdefined(C, f) end + # generated structs mirror the schemas + @test fieldnames(C.TWidget) == (:id, :tags) + @test fieldtype(C.TWidget, :id) == Int64 + @test fieldtype(C.TWidget, :tags) == Vector{String} + @test fieldtype(C.TOrder, :note) == Union{C.Absent,Nothing,String} + @test fieldtype(C.TOrder, :placed) == Dates.Date + @test fieldtype(C.TOrder, :widget) == C.TWidget + + # required query params are required keywords; optional default to nothing + m = only(methods(C.getwidget)) + @test Base.kwarg_decl(m) isa Vector + @test :owner in Base.kwarg_decl(m) end - run_tests_with_servers && sleep(20) # avoid port conflicts +end # @testset "OpenAPI" +include("schema_engine/resources.jl") +include("schema_engine/compiled.jl") +include("schema_engine/rebase.jl") +if get(ENV, "OPENAPI_SCHEMA_SUITE", "") == "all" + include("schema_engine/official.jl") +end +include("normalization.jl") +include("models.jl") +include("discriminators.jl") +include("references.jl") +include("semantics.jl") +include("runtime.jl") +include("runtime_integration.jl") +include("trim_compile_tests.jl") +if haskey(ENV, "OPENAPI_CORPUS_TESTS") + include("corpus.jl") end diff --git a/test/runtime.jl b/test/runtime.jl new file mode 100644 index 0000000..68651f7 --- /dev/null +++ b/test/runtime.jl @@ -0,0 +1,226 @@ +@testset "generated runtime units" begin + document = OpenAPI.obj( + "openapi" => "3.2.0", + "info" => OpenAPI.obj("title" => "Runtime", "version" => "1"), + "paths" => OpenAPI.obj( + "/noop" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "noop", + "responses" => OpenAPI.obj( + "204" => OpenAPI.obj("description" => "empty"), + ), + ), + ), + ), + ) + source = OpenAPI.client(document; name = "RuntimeUnitClient") + host = Module(:RuntimeUnitClientHost) + Base.include_string(host, source, "RuntimeUnitClient.jl") + C = Base.invokelatest(getfield, host, :RuntimeUnitClient) + + invoke(function_name, args...; kwargs...) = + Base.invokelatest(getfield(C, function_name), args...; kwargs...) + + @testset "path styles" begin + @test invoke(:_path_parameter, "id", ["a", "b"], :simple, false) == "a,b" + @test invoke( + :_path_parameter, + "id", + Dict("role" => "admin", "first" => "Alex"), + :simple, + true, + ) in ("role=admin,first=Alex", "first=Alex,role=admin") + @test invoke(:_path_parameter, "id", ["a", "b"], :label, true) == ".a.b" + @test invoke(:_path_parameter, "id", ["a", "b"], :matrix, true) == + ";id=a;id=b" + @test invoke(:_path_parameter, "id", nothing, :matrix, false) == ";id" + @test invoke(:_path_parameter, "id", "a/b", :simple, false) == "a%2Fb" + end + + @testset "query, header, and cookie styles" begin + @test invoke(:_query_parameter, "q", ["a", "b"], :form, false, false) == + [("q", "a,b", true)] + @test invoke(:_query_parameter, "q", ["a", "b"], :form, true, false) == + [("q", "a", false), ("q", "b", false)] + @test invoke( + :_query_parameter, + "q", + ["a", "b"], + :spaceDelimited, + false, + false, + ) == [("q", "a b", false)] + @test invoke( + :_query_parameter, + "q", + ["a", "b"], + :pipeDelimited, + false, + false, + ) == [("q", "a|b", false)] + deep = invoke( + :_query_parameter, + "filter", + Dict("role" => "admin"), + :deepObject, + true, + false, + ) + @test deep == [("filter[role]", "admin", false)] + @test invoke( + :_query_parameter, + "expand", + ["customer", "invoice"], + :deepObject, + true, + false, + ) == [ + ("expand[]", "customer", false), + ("expand[]", "invoice", false), + ] + @test invoke(:_query_parameter, "created", 7, :deepObject, true, false) == + [("created", "7", false)] + @test_throws ArgumentError invoke( + :_query_parameter, + "filter", + Dict("nested" => Dict("x" => 1)), + :deepObject, + true, + false, + ) + + @test invoke(:_header_parameter, ["a", "b"], false) == "a,b" + @test invoke(:_cookie_parameter, "id", ["a", "b"], :form, true, false) == + [("id=a&id=b", "", true, true)] + @test invoke(:_cookie_parameter, "id", ["a", "b"], :cookie, false, false) == + [("id", "a,b", true, false)] + end + + @testset "form defaults use content-based serialization" begin + pairs = invoke( + :_form_pairs, + C.DEFAULT_CLIENT, + Dict( + "address" => Dict("city" => "Salt Lake City"), + "items" => [Dict("id" => 1), Dict("id" => 2)], + ), + (), + ) + @test ("address", "{\"city\":\"Salt Lake City\"}", false, false) in pairs + @test count(pair -> pair[1] == "items", pairs) == 2 + @test ("items", "{\"id\":1}", false, false) in pairs + @test ("items", "{\"id\":2}", false, false) in pairs + end + + @testset "URI and header safety" begin + reserved = ":/?#[]@!\$&'()*+,;=" + @test invoke(:_escape, reserved; allow_reserved = true) == reserved + @test invoke(:_escape, "%2F"; allow_reserved = true) == "%2F" + @test invoke(:_escape, "a b") == "a%20b" + @test invoke(:_safe_header, "X-Test", "ok") == ("X-Test" => "ok") + @test_throws ArgumentError invoke(:_safe_header, "Bad Header", "ok") + @test_throws ArgumentError invoke(:_safe_header, "X-Test", "ok\r\nInjected: x") + end + + @testset "media and response selection" begin + @test invoke(:_media_match_score, "application/problem+json", "application/*+json") == + 3 + @test invoke(:_media_match_score, "text/plain", "text/*") == 2 + @test invoke(:_media_match_score, "application/json", "*/*") == 1 + @test invoke(:_media_match_score, "application/json", "text/*") == 0 + + responses = ( + (selector = "default", media = (), headers = ()), + (selector = "2XX", media = (), headers = ()), + (selector = "201", media = (), headers = ()), + ) + @test invoke(:_select_response, responses, 201).selector == "201" + @test invoke(:_select_response, responses, 202).selector == "2XX" + @test invoke(:_select_response, responses, 404).selector == "default" + end + + @testset "Set-Cookie response headers stay line-separated" begin + headers = [ + "Set-Cookie" => "lang=en-US; Expires=Wed, 09 Jun 2021 10:18:14 GMT", + "Set-Cookie" => "foo=bar; Expires=Wed, 09 Jun 2021 10:18:14 GMT", + ] + schema_descriptor = ( + headers = ( + ( + name = "Set-Cookie", + type = Dict{String,String}, + required = true, + shape = :object, + explode = true, + schema = nothing, + content = (), + ), + ), + ) + decoded = invoke( + :_decode_response_headers, + C.DEFAULT_CLIENT, + schema_descriptor, + headers, + ) + @test decoded["Set-Cookie"]["lang"] == + "en-US; Expires=Wed, 09 Jun 2021 10:18:14 GMT" + @test decoded["Set-Cookie"]["foo"] == + "bar; Expires=Wed, 09 Jun 2021 10:18:14 GMT" + + content_descriptor = ( + headers = ( + ( + name = "Set-Cookie", + type = String, + required = true, + shape = :scalar, + explode = false, + schema = nothing, + content = (("text/plain", String, nothing, ()),), + ), + ), + ) + content = invoke( + :_decode_response_headers, + C.DEFAULT_CLIENT, + content_descriptor, + headers, + ) + @test content["Set-Cookie"] == join(last.(headers), '\n') + end + + @testset "strict and binary-safe decoding" begin + @test_throws C.DecodeError invoke( + :_parse_json, + Vector{UInt8}([0xff]), + "testing", + ) + @test_throws C.DecodeError invoke( + :_parse_json, + "{\"x\":1,\"x\":2}", + "testing", + ) + @test_throws C.DecodeError invoke( + :_decode_body, + C.DEFAULT_CLIENT, + String, + "text/plain", + UInt8[0xff], + nothing, + ) + + error = C.ApiError( + "noop", + 500, + Pair{String,String}[], + Dict{String,Any}(), + UInt8[0xff, 0x00], + nothing, + nothing, + ) + shown = sprint(showerror, error) + @test occursin("2 binary bytes", shown) + @test occursin("ff00", shown) + end +end diff --git a/test/runtime_integration.jl b/test/runtime_integration.jl new file mode 100644 index 0000000..13baa22 --- /dev/null +++ b/test/runtime_integration.jl @@ -0,0 +1,878 @@ +function runtime_response( + media::AbstractString, + schema; + description::AbstractString = "response", + headers = nothing, +) + response = OpenAPI.obj( + "description" => String(description), + "content" => OpenAPI.obj( + String(media) => OpenAPI.obj("schema" => schema), + ), + ) + headers === nothing || (response["headers"] = headers) + return response +end + +function runtime_parameter( + name, + location, + schema; + required = true, + style = nothing, + explode = nothing, + allow_reserved = false, +) + parameter = OpenAPI.obj( + "name" => String(name), + "in" => String(location), + "required" => required, + "schema" => schema, + ) + style === nothing || (parameter["style"] = String(style)) + explode === nothing || (parameter["explode"] = explode) + allow_reserved && (parameter["allowReserved"] = true) + return parameter +end + +function captured_header(request, name::AbstractString, default = "") + lowered = lowercase(name) + index = findlast(pair -> lowercase(pair.first) == lowered, request.headers) + return index === nothing ? default : request.headers[index].second +end + +function captured_header_values(request, name::AbstractString) + lowered = lowercase(name) + return String[ + pair.second for pair in request.headers if lowercase(pair.first) == lowered + ] +end + +@testset "generated runtime HTTP integration" begin + captures = Channel{Any}(64) + handler = function (request) + target = String(request.target) + path = first(split(target, '?'; limit = 2)) + body = Vector{UInt8}(codeunits(String(request.body))) + put!( + captures, + ( + method = String(request.method), + target, + headers = Pair{String,String}[ + String(key) => String(value) for (key, value) in request.headers + ], + body, + ), + ) + if startswith(path, "/status/201") + return HTTP.Response( + 201, + ["Content-Type" => "application/json", "X-Rate" => "7"], + """{"kind":"exact","id":1}""", + ) + elseif startswith(path, "/status/202") + return HTTP.Response( + 202, + ["Content-Type" => "application/vnd.runtime+json", "X-Rate" => "8"], + """{"kind":"range","queued":true}""", + ) + elseif startswith(path, "/failure/duplicate") + return HTTP.Response( + 500, + ["Content-Type" => "application/json"], + """{"error":"first","error":"second"}""", + ) + elseif startswith(path, "/failure/binary") + return HTTP.Response( + 500, + ["Content-Type" => "text/plain"], + UInt8[0xff, 0x00], + ) + elseif startswith(path, "/unexpected") + return HTTP.Response(200, ["Content-Type" => "text/plain"], "surprise") + elseif startswith(path, "/wrong-content") + return HTTP.Response(200, ["Content-Type" => "text/html"], "

wrong

") + elseif startswith(path, "/bad-text") + return HTTP.Response( + 200, + ["Content-Type" => "text/plain"], + UInt8[0xff], + ) + elseif startswith(path, "/sequence") + return HTTP.Response( + 200, + ["Content-Type" => "application/x-ndjson"], + "1\n2\n", + ) + elseif startswith(path, "/custom") + return HTTP.Response( + 200, + ["Content-Type" => "application/x-runtime"], + body, + ) + elseif startswith(path, "/json") + return HTTP.Response(200, ["Content-Type" => "application/json"], body) + elseif startswith(path, "/form") || startswith(path, "/multipart") + return HTTP.Response(200, ["Content-Type" => "text/plain"], "accepted") + elseif startswith(path, "/secure") + return HTTP.Response(200, ["Content-Type" => "text/plain"], "authorized") + end + return HTTP.Response( + 200, + ["Content-Type" => "application/json"], + """{"ok":true}""", + ) + end + + server = HTTP.serve!(handler, "127.0.0.1", 0; verbose = false) + try + port = HTTP.port(server) + base = "http://127.0.0.1:$port" + string_schema = OpenAPI.obj("type" => "string") + string_array = OpenAPI.obj( + "type" => "array", + "items" => string_schema, + ) + payload_schema = OpenAPI.obj( + "type" => "object", + "required" => ["name", "count"], + "properties" => OpenAPI.obj( + "name" => string_schema, + "count" => OpenAPI.obj("type" => "integer"), + ), + "additionalProperties" => false, + ) + form_schema = OpenAPI.obj( + "type" => "object", + "required" => ["tags", "flag"], + "properties" => OpenAPI.obj( + "tags" => string_array, + "flag" => OpenAPI.obj("type" => "boolean"), + "note" => string_schema, + ), + "additionalProperties" => false, + ) + nested_multipart_schema = OpenAPI.obj( + "type" => "object", + "required" => ["nested_file", "label"], + "properties" => OpenAPI.obj( + "nested_file" => OpenAPI.obj( + "type" => "string", + "format" => "binary", + ), + "label" => string_schema, + ), + "additionalProperties" => false, + ) + multipart_schema = OpenAPI.obj( + "type" => "object", + "required" => ["file", "note", "bundle"], + "properties" => OpenAPI.obj( + "file" => OpenAPI.obj("type" => "string", "format" => "binary"), + "note" => string_schema, + "bundle" => nested_multipart_schema, + ), + "additionalProperties" => false, + ) + exact_schema = OpenAPI.obj( + "type" => "object", + "required" => ["kind", "id"], + "properties" => OpenAPI.obj( + "kind" => OpenAPI.obj("const" => "exact"), + "id" => OpenAPI.obj("type" => "integer"), + ), + "additionalProperties" => false, + ) + range_schema = OpenAPI.obj( + "type" => "object", + "required" => ["kind", "queued"], + "properties" => OpenAPI.obj( + "kind" => OpenAPI.obj("const" => "range"), + "queued" => OpenAPI.obj("type" => "boolean"), + ), + "additionalProperties" => false, + ) + + paths = OpenAPI.obj( + "/styles/{simple}/{label}/{matrix}" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "serializeStyles", + "parameters" => Any[ + runtime_parameter( + "simple", + "path", + string_array; + style = "simple", + explode = false, + ), + runtime_parameter( + "label", + "path", + string_schema; + style = "label", + explode = false, + ), + runtime_parameter( + "matrix", + "path", + string_array; + style = "matrix", + explode = true, + ), + runtime_parameter( + "qform", + "query", + string_array; + style = "form", + explode = false, + ), + runtime_parameter( + "spaces", + "query", + string_array; + style = "spaceDelimited", + explode = false, + ), + runtime_parameter( + "pipes", + "query", + string_array; + style = "pipeDelimited", + explode = false, + ), + runtime_parameter( + "reserved", + "query", + string_schema; + style = "form", + allow_reserved = true, + ), + runtime_parameter( + "xmeta", + "header", + string_schema; + style = "simple", + ), + runtime_parameter( + "crumb", + "cookie", + string_array; + style = "form", + explode = true, + ), + ], + "responses" => OpenAPI.obj( + "200" => runtime_response("application/json", OpenAPI.obj()), + ), + ), + ), + "/json" => OpenAPI.obj( + "post" => OpenAPI.obj( + "operationId" => "submitJson", + "requestBody" => OpenAPI.obj( + "required" => true, + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "\$ref" => "#/components/schemas/Payload", + ), + ), + ), + ), + "responses" => OpenAPI.obj( + "200" => runtime_response( + "application/json", + OpenAPI.obj("\$ref" => "#/components/schemas/Payload"), + ), + ), + ), + ), + "/form" => OpenAPI.obj( + "post" => OpenAPI.obj( + "operationId" => "submitForm", + "requestBody" => OpenAPI.obj( + "required" => true, + "content" => OpenAPI.obj( + "application/x-www-form-urlencoded" => OpenAPI.obj( + "schema" => form_schema, + "encoding" => OpenAPI.obj( + "tags" => OpenAPI.obj( + "style" => "form", + "explode" => true, + ), + ), + ), + ), + ), + "responses" => OpenAPI.obj( + "200" => runtime_response("text/plain", string_schema), + ), + ), + ), + "/multipart" => OpenAPI.obj( + "post" => OpenAPI.obj( + "operationId" => "submitMultipart", + "requestBody" => OpenAPI.obj( + "required" => true, + "content" => OpenAPI.obj( + "multipart/form-data" => OpenAPI.obj( + "schema" => multipart_schema, + "encoding" => OpenAPI.obj( + "file" => OpenAPI.obj( + "contentType" => "application/octet-stream, image/png", + "headers" => OpenAPI.obj( + "X-Part-Index" => OpenAPI.obj( + "required" => true, + "schema" => OpenAPI.obj( + "type" => "integer", + ), + ), + "X-Part-Meta" => OpenAPI.obj( + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "type" => "object", + ), + ), + ), + ), + ), + ), + "bundle" => OpenAPI.obj( + "contentType" => "multipart/form-data", + "encoding" => OpenAPI.obj( + "nested_file" => OpenAPI.obj( + "contentType" => "application/octet-stream", + "headers" => OpenAPI.obj( + "X-Nested" => OpenAPI.obj( + "required" => true, + "schema" => OpenAPI.obj( + "type" => "integer", + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + "responses" => OpenAPI.obj( + "200" => runtime_response("text/plain", string_schema), + ), + ), + ), + "/status/{code}" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "statusResult", + "parameters" => Any[ + runtime_parameter( + "code", + "path", + OpenAPI.obj("type" => "integer"), + ), + ], + "responses" => OpenAPI.obj( + "201" => runtime_response( + "application/json", + exact_schema; + headers = OpenAPI.obj( + "X-Rate" => OpenAPI.obj( + "required" => true, + "schema" => OpenAPI.obj("type" => "integer"), + ), + ), + ), + "2XX" => runtime_response( + "application/*+json", + range_schema; + headers = OpenAPI.obj( + "X-Rate" => OpenAPI.obj( + "required" => true, + "schema" => OpenAPI.obj("type" => "integer"), + ), + ), + ), + "default" => runtime_response( + "application/problem+json", + OpenAPI.obj(), + ), + ), + ), + ), + "/failure/{kind}" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "failure", + "parameters" => Any[ + runtime_parameter("kind", "path", string_schema), + ], + "responses" => OpenAPI.obj( + "default" => OpenAPI.obj( + "description" => "failure", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj(), + ), + "text/plain" => OpenAPI.obj( + "schema" => string_schema, + ), + ), + ), + ), + ), + ), + "/unexpected" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "unexpectedBody", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj("description" => "empty"), + ), + ), + ), + "/wrong-content" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "wrongContent", + "responses" => OpenAPI.obj( + "200" => runtime_response("application/json", OpenAPI.obj()), + ), + ), + ), + "/bad-text" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "badText", + "responses" => OpenAPI.obj( + "200" => runtime_response("text/plain", string_schema), + ), + ), + ), + "/sequence" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "sequence", + "responses" => OpenAPI.obj( + "200" => runtime_response( + "application/x-ndjson", + OpenAPI.obj( + "type" => "array", + "items" => OpenAPI.obj("type" => "integer"), + ), + ), + ), + ), + ), + "/custom" => OpenAPI.obj( + "post" => OpenAPI.obj( + "operationId" => "customCodec", + "requestBody" => OpenAPI.obj( + "required" => true, + "content" => OpenAPI.obj( + "application/x-runtime" => OpenAPI.obj( + "schema" => string_schema, + ), + ), + ), + "responses" => OpenAPI.obj( + "200" => runtime_response( + "application/x-runtime", + string_schema, + ), + ), + ), + ), + ) + + security_response = OpenAPI.obj( + "200" => runtime_response("text/plain", string_schema), + ) + paths["/secure/and"] = OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "secureAnd", + "security" => Any[ + OpenAPI.obj("HeaderKey" => String[], "CookieKey" => String[]), + ], + "responses" => security_response, + ), + ) + paths["/secure/or"] = OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "secureOr", + "security" => Any[ + OpenAPI.obj("QueryKey" => String[]), + OpenAPI.obj("OAuth" => ["read"]), + ], + "responses" => security_response, + ), + ) + paths["/secure/basic"] = OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "secureBasic", + "security" => Any[OpenAPI.obj("Basic" => String[])], + "responses" => security_response, + ), + ) + paths["/secure/custom"] = OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "secureCustom", + "security" => Any[OpenAPI.obj("Signature" => String[])], + "responses" => security_response, + ), + ) + paths["/secure/mtls"] = OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "secureMtls", + "security" => Any[OpenAPI.obj("Mutual" => ["admin"])], + "responses" => security_response, + ), + ) + + document = OpenAPI.obj( + "openapi" => "3.2.0", + "info" => OpenAPI.obj("title" => "Runtime API", "version" => "1"), + "servers" => Any[ + OpenAPI.obj( + "name" => "local", + "url" => "http://127.0.0.1:{port}", + "variables" => OpenAPI.obj( + "port" => OpenAPI.obj( + "default" => string(port), + "enum" => [string(port)], + ), + ), + ), + OpenAPI.obj("name" => "relative", "url" => "/v2"), + ], + "paths" => paths, + "components" => OpenAPI.obj( + "schemas" => OpenAPI.obj("Payload" => payload_schema), + "securitySchemes" => OpenAPI.obj( + "HeaderKey" => OpenAPI.obj( + "type" => "apiKey", + "in" => "header", + "name" => "X-API-Key", + ), + "CookieKey" => OpenAPI.obj( + "type" => "apiKey", + "in" => "cookie", + "name" => "sid", + ), + "QueryKey" => OpenAPI.obj( + "type" => "apiKey", + "in" => "query", + "name" => "access_key", + ), + "Basic" => OpenAPI.obj("type" => "http", "scheme" => "basic"), + "OAuth" => OpenAPI.obj( + "type" => "oauth2", + "flows" => OpenAPI.obj( + "clientCredentials" => OpenAPI.obj( + "tokenUrl" => "https://auth.example/token", + "scopes" => OpenAPI.obj("read" => "read access"), + ), + ), + ), + "Signature" => OpenAPI.obj( + "type" => "http", + "scheme" => "Signature", + ), + "Mutual" => OpenAPI.obj("type" => "mutualTLS"), + ), + ), + ) + + source = OpenAPI.client(document; name = "RuntimeHTTPClient") + @test source == OpenAPI.client(document; name = "RuntimeHTTPClient") + host = Module(:RuntimeHTTPClientHost) + Base.include_string(host, source, "RuntimeHTTPClient.jl") + C = Base.invokelatest(getfield, host, :RuntimeHTTPClient) + + call(name, args...; kwargs...) = + Base.invokelatest(getfield(C, name), args...; kwargs...) + take_request() = take!(captures) + client = C.Client() + + @testset "parameters, servers, and request overrides" begin + result = call( + :serializestyles, + ["a", "b"], + "dot/slash", + ["x", "y"]; + qform = ["a", "b"], + spaces = ["a", "b"], + pipes = ["a", "b"], + reserved = "a/b:c", + xmeta = "metadata", + crumb = ["c", "d"], + client, + request_headers = ["X-Request" => "yes", "Accept" => "custom/type"], + request_options = (; status_exception = true), + with_http_info = true, + ) + request = take_request() + @test request.method == "GET" + @test request.target == + "/styles/a,b/.dot%2Fslash/;matrix=x;matrix=y?qform=a,b&spaces=a%20b&pipes=a%7Cb&reserved=a/b:c" + @test captured_header(request, "xmeta") == "metadata" + @test captured_header(request, "X-Request") == "yes" + @test captured_header(request, "Cookie") == "crumb=c&crumb=d" + @test captured_header(request, "Accept") == "custom/type" + @test result.status == 200 + @test result.body["ok"] === true + + operation = C._OP_serializestyles + relative = C.Client(; server_name = "relative") + @test call(:_server_for, relative, operation) == base * "/v2" + @test_throws ArgumentError call( + :_server_for, + C.Client(; server_name = "missing"), + operation, + ) + @test_throws ArgumentError call( + :_server_for, + C.Client(; server_variables = Dict("port" => "1")), + operation, + ) + end + + @testset "JSON, form, multipart, sequential, and custom bodies" begin + payload_type = first(C._OP_submitjson.request.media)[2] + payload = call(:_decode, payload_type, Dict("name" => "Ada", "count" => 2)) + decoded = call( + :submitjson, + payload; + client, + request_options = (; body = "wrong"), + ) + request = take_request() + @test JSON.parse(String(request.body)) == Dict("name" => "Ada", "count" => 2) + @test decoded.name == "Ada" + @test decoded.count == 2 + + form_type = first(C._OP_submitform.request.media)[2] + form = Base.invokelatest(form_type; tags = ["a", "b"], flag = true) + @test call(:submitform, form; client) == "accepted" + request = take_request() + form_text = String(request.body) + @test occursin("tags=a", form_text) + @test occursin("tags=b", form_text) + @test occursin("flag=true", form_text) + @test captured_header(request, "Content-Type") == + "application/x-www-form-urlencoded" + + multipart_type = first(C._OP_submitmultipart.request.media)[2] + upload = C.Upload( + UInt8[0x00, 0x01, 0xff]; + filename = "data.bin", + content_type = "application/octet-stream", + headers = ["X-Part" => "yes"], + ) + multipart = Base.invokelatest( + multipart_type; + file = upload, + note = "hello", + bundle = Base.invokelatest( + fieldtype(multipart_type, :bundle); + nested_file = C.Upload( + UInt8[0x02, 0x03]; + filename = "nested.bin", + ), + label = "inside", + ), + ) + @test call( + :submitmultipart, + multipart; + client, + multipart_headers = Dict( + "file" => Dict( + "X-Part-Index" => 7, + "x-part-meta" => Dict("source" => "test"), + ), + "bundle" => C.MultipartPartHeaders( + ; + parts = Dict( + "nested_file" => Dict("X-Nested" => 9), + ), + ), + ), + ) == "accepted" + request = take_request() + multipart_text = String(request.body) + @test occursin("filename=\"data.bin\"", multipart_text) + @test occursin("Content-Type: application/octet-stream", multipart_text) + @test occursin("X-Part: yes", multipart_text) + @test occursin("X-Part-Index: 7", multipart_text) + @test occursin("X-Part-Meta: {\"source\":\"test\"}", multipart_text) + @test occursin("filename=\"nested.bin\"", multipart_text) + @test occursin("X-Nested: 9", multipart_text) + @test occursin("inside", multipart_text) + @test occursin("hello", multipart_text) + @test startswith( + captured_header(request, "Content-Type"), + "multipart/form-data; boundary=", + ) + missing_header = @test_throws ArgumentError call( + :submitmultipart, + multipart; + client, + ) + @test occursin("required multipart header", missing_header.value.msg) + unknown_header = @test_throws ArgumentError call( + :submitmultipart, + multipart; + client, + multipart_headers = Dict( + "file" => Dict( + "X-Part-Index" => 7, + "X-Unknown" => "no", + ), + ), + ) + @test occursin("not documented", unknown_header.value.msg) + + @test call(:sequence; client) == [1, 2] + take_request() + + @test call(:customcodec, "MiXeD"; client) == "MiXeD" + request = take_request() + @test String(request.body) == "MiXeD" + C.codec!( + client, + "application/x-runtime"; + encode = (value, _) -> uppercase(value), + decode = (bytes, _) -> lowercase(String(bytes)), + ) + @test call(:customcodec, "MiXeD"; client) == "mixed" + request = take_request() + @test String(request.body) == "MIXED" + end + + @testset "response selection, headers, and safe errors" begin + exact = call(:statusresult, 201; client, with_http_info = true) + take_request() + @test exact.status == 201 + @test exact.body.kind == "exact" + @test exact.body.id == 1 + @test exact.decoded_headers["X-Rate"] == 7 + + range = call(:statusresult, 202; client, with_http_info = true) + take_request() + @test range.status == 202 + @test range.body.kind == "range" + @test range.body.queued === true + @test range.decoded_headers["X-Rate"] == 8 + + duplicate = @test_throws C.ApiError call( + :failure, + "duplicate"; + client, + request_options = (; status_exception = true, retry = false), + ) + take_request() + @test duplicate.value.status == 500 + @test duplicate.value.decoded === nothing + @test duplicate.value.decode_error isa C.DecodeError + @test occursin("duplicate JSON object key", sprint(showerror, duplicate.value)) + + binary = @test_throws C.ApiError call( + :failure, + "binary"; + client, + request_options = (; retry = false), + ) + take_request() + @test binary.value.body == UInt8[0xff, 0x00] + @test binary.value.decode_error isa C.DecodeError + @test occursin("binary bytes", sprint(showerror, binary.value)) + + @test_throws C.UnexpectedBody call(:unexpectedbody; client) + take_request() + @test_throws C.UnexpectedContentType call(:wrongcontent; client) + take_request() + @test_throws C.DecodeError call(:badtext; client) + take_request() + end + + @testset "security OR, AND, roles, and credential types" begin + secure_client = C.Client() + @test_throws ArgumentError call(:secureand; client = secure_client) + C.credential!(secure_client, "HeaderKey", C.ApiKeyCredential("header")) + @test_throws ArgumentError call(:secureand; client = secure_client) + C.credential!(secure_client, "CookieKey", C.ApiKeyCredential("cookie")) + @test call(:secureand; client = secure_client) == "authorized" + request = take_request() + @test captured_header(request, "X-API-Key") == "header" + @test captured_header(request, "Cookie") == "sid=cookie" + + C.clearcredential!(secure_client, "HeaderKey") + C.clearcredential!(secure_client, "CookieKey") + C.credential!( + secure_client, + "OAuth", + C.BearerCredential("token"; scopes = ["wrong"]), + ) + @test_throws ArgumentError call(:secureor; client = secure_client) + C.credential!( + secure_client, + "OAuth", + C.BearerCredential("token"; scopes = ["read"]), + ) + @test call(:secureor; client = secure_client) == "authorized" + request = take_request() + @test captured_header(request, "Authorization") == "Bearer token" + + C.clearcredential!(secure_client, "OAuth") + C.credential!(secure_client, "QueryKey", C.ApiKeyCredential("query")) + @test call(:secureor; client = secure_client) == "authorized" + request = take_request() + @test endswith(request.target, "?access_key=query") + + empty!(secure_client.credentials) + C.credential!( + secure_client, + "Basic", + C.BasicCredential("user", "password"), + ) + @test call(:securebasic; client = secure_client) == "authorized" + request = take_request() + @test captured_header(request, "Authorization") == + "Basic " * C.Base64.base64encode("user:password") + + empty!(secure_client.credentials) + C.credential!( + secure_client, + "Signature", + C.HttpCredential("signed"), + ) + @test call(:securecustom; client = secure_client) == "authorized" + request = take_request() + @test captured_header(request, "Authorization") == + "Signature signed" + + empty!(secure_client.credentials) + C.credential!( + secure_client, + "Mutual", + C.MutualTLSCredential( + (; connect_timeout = 2); + roles = ["admin"], + ), + ) + query = Tuple{String,String,Bool,Bool}[] + headers = Pair{String,String}[] + cookies = Tuple{String,String,Bool,Bool}[] + options = call( + :_security!, + secure_client, + C._OP_securemtls.security, + query, + headers, + cookies, + NamedTuple(), + ) + @test options.connect_timeout == 2 + end + finally + close(server) + end +end diff --git a/test/schema_engine/compiled.jl b/test/schema_engine/compiled.jl new file mode 100644 index 0000000..829076c --- /dev/null +++ b/test/schema_engine/compiled.jl @@ -0,0 +1,599 @@ +struct SuiteRetriever <: Resources.AbstractRetriever + remotes::String +end + +function Resources.retrieve(retriever::SuiteRetriever, id::Resources.ResourceId) + uri = id.uri + if lowercase(uri.host) == "localhost" + relative = lstrip(Resources.URIs.unescapeuri(uri.path), '/') + file = joinpath(retriever.remotes, relative) + isfile(file) || + throw(Resources.RetrievalError(id, "suite remote does not exist")) + return Resources.RetrievedResource( + id, + read(file); + media_type = "application/schema+json", + ) + end + io = IOBuffer() + response = Downloads.request(string(id); output = io, throw = false) + if response isa Downloads.Response && response.status == 200 + return Resources.RetrievedResource( + id, + take!(seekstart(io)); + media_type = "application/schema+json", + ) + end + return throw(Resources.RetrievalError(id, "HTTP retrieval failed")) +end + +@testset "Compiled schema resources" begin + source = Dict( + "\$schema" => "https://json-schema.org/draft/2020-12/schema", + "\$id" => "https://example.com/root", + "\$defs" => Dict( + "text" => Dict("\$anchor" => "text", "type" => "string"), + "nested" => Dict( + "\$id" => "nested", + "type" => "object", + "properties" => Dict("value" => Dict("\$ref" => "#value")), + "\$defs" => Dict( + "value" => Dict( + "\$dynamicAnchor" => "value", + "type" => "integer", + ), + ), + ), + ), + "\$ref" => "#text", + "minLength" => 2, + ) + original = deepcopy(source) + compiled = SchemaEngine.CompiledSchema(source) + @test source == original + @test compiled.dialect === SchemaEngine.DRAFT202012 + @test string(compiled.root.resource) == "https://example.com/root" + @test haskey( + compiled.registry, + Resources.ResourceId("https://example.com/nested"), + ) + @test isvalid(compiled, "ok") + @test !isvalid(compiled, "x") + @test !isvalid(compiled, 1) + + legacy = SchemaEngine.CompiledSchema( + Dict( + "\$ref" => "#/definitions/text", + "minLength" => 3, + "definitions" => Dict("text" => Dict("type" => "string")), + ), + ) + @test isvalid(legacy, "x") + @test !isvalid(legacy, 1) +end + +@testset "Compiled schema retrieval policy" begin + schema = Dict("\$ref" => "https://example.com/string") + @test_throws SchemaEngine.CompilationError SchemaEngine.CompiledSchema(schema) + retriever = Resources.MemoryRetriever( + Dict("https://example.com/string" => "{\"type\":\"string\"}"), + ) + compiled = SchemaEngine.CompiledSchema(schema; retriever) + @test isvalid(compiled, "text") + @test !isvalid(compiled, 1) + + requested = Resources.ResourceId("https://example.com/redirect") + final = Resources.ResourceId("https://cdn.example.com/string") + redirected = Resources.MemoryRetriever( + Dict( + requested => Resources.RetrievedResource( + final, + Vector{UInt8}(codeunits("{\"type\":\"string\"}")), + ), + ), + ) + redirected_schema = SchemaEngine.CompiledSchema( + Dict("\$ref" => string(requested)); + retriever = redirected, + ) + @test isvalid(redirected_schema, "text") +end + +@testset "Compiled schema bounds and diagnostics" begin + @test_throws SchemaEngine.CompilationError SchemaEngine.CompiledSchema( + Dict("type" => "string"); + max_nodes = 1, + ) + nested = + Dict("allOf" => Any[Dict("allOf" => Any[Dict("type" => "string")])]) + @test_throws SchemaEngine.CompilationError SchemaEngine.CompiledSchema( + nested; + max_depth = 2, + ) + cyclic = Dict{String,Any}() + cyclic["not"] = cyclic + @test_throws SchemaEngine.CompilationError SchemaEngine.CompiledSchema(cyclic) + + compiled = SchemaEngine.CompiledSchema( + Dict( + "\$schema" => SchemaEngine.DRAFT202012.uri, + "properties" => Dict("a/b" => Dict("type" => "integer")), + ), + ) + issue = SchemaEngine.validate(compiled, Dict("a/b" => "wrong")) + @test issue.path == "/a~1b" + + legacy_unknown = SchemaEngine.CompiledSchema( + Dict( + "\$schema" => SchemaEngine.DRAFT7.uri, + "\$defs" => Dict( + "not-a-schema" => Dict( + "\$id" => "https://example.com/not-a-resource", + ), + ), + ), + ) + @test !haskey( + legacy_unknown.registry, + Resources.ResourceId("https://example.com/not-a-resource"), + ) +end + +@testset "Dialect keyword isolation" begin + draft4_if = SchemaEngine.CompiledSchema( + Dict( + "if" => Dict("\$ref" => "https://unregistered.example/schema"), + "then" => Dict("type" => "string"), + ); + dialect = SchemaEngine.DRAFT4, + ) + @test isvalid(draft4_if, 1) + + removed_dependency = SchemaEngine.CompiledSchema( + Dict("dependencies" => Dict("a" => ["b"])); + dialect = SchemaEngine.DRAFT202012, + ) + @test isvalid(removed_dependency, Dict("a" => 1)) + + modern_child = Dict( + "\$schema" => SchemaEngine.DRAFT7.uri, + "\$id" => "https://example.com/modern-child", + "type" => "string", + ) + cross_dialect = SchemaEngine.CompiledSchema( + Dict( + "id" => "https://example.com/legacy-root", + "definitions" => Dict("child" => modern_child), + "allOf" => + Any[Dict("\$ref" => "https://example.com/modern-child")], + ); + dialect = SchemaEngine.DRAFT4, + ) + @test isvalid(cross_dialect, "text") + @test !isvalid(cross_dialect, 1) + + for schema_dialect in (SchemaEngine.DRAFT6, SchemaEngine.DRAFT7) + anchored = SchemaEngine.CompiledSchema( + Dict("\$id" => "#root-anchor", "type" => "integer"); + dialect = schema_dialect, + ) + resolved = Resources.resolve( + anchored.registry, + Resources.Reference(anchored.root.resource, "#root-anchor"), + ) + @test resolved.id == anchored.root + end +end + +@testset "Compiled evaluation safety" begin + cycle = SchemaEngine.CompiledSchema( + Dict("\$ref" => "#"); + dialect = SchemaEngine.DRAFT202012, + ) + @test_throws SchemaEngine.EvaluationError SchemaEngine.validate(cycle, 42) + + nested = SchemaEngine.CompiledSchema( + Dict("items" => Dict("items" => Dict("type" => "integer"))); + dialect = SchemaEngine.DRAFT202012, + ) + @test_throws SchemaEngine.EvaluationError SchemaEngine.validate( + nested, + Any[Any[1]]; + max_evaluations = 2, + ) + @test_throws SchemaEngine.CompilationError SchemaEngine.CompiledSchema( + Dict("pattern" => "["); + dialect = SchemaEngine.DRAFT202012, + ) + @test_throws SchemaEngine.CompilationError SchemaEngine.CompiledSchema( + Dict("contains" => Dict(), "minContains" => "invalid"); + dialect = SchemaEngine.DRAFT202012, + ) + + many_failures = SchemaEngine.CompiledSchema( + Dict("allOf" => Any[false for _ in 1:10]); + dialect = SchemaEngine.DRAFT202012, + ) + @test SchemaEngine.validate(many_failures, 1; max_issues = 1) !== nothing + @test_throws SchemaEngine.EvaluationError SchemaEngine.validate( + many_failures, + 1; + fail_fast = false, + max_issues = 1, + ) + speculative = SchemaEngine.CompiledSchema( + Dict("anyOf" => Any[[false for _ in 1:10]; true]); + dialect = SchemaEngine.DRAFT202012, + ) + @test isempty( + SchemaEngine.validate(speculative, 1; fail_fast = false, max_issues = 1), + ) + + short_circuit = SchemaEngine.CompiledSchema( + Dict( + "\$schema" => SchemaEngine.DRAFT202012.uri, + "type" => "string", + "allOf" => [Dict("\$ref" => "#")], + ), + ) + issue = SchemaEngine.validate(short_circuit, 1) + @test issue.reason == "type" + @test_throws SchemaEngine.EvaluationError SchemaEngine.validate( + short_circuit, + 1; + fail_fast = false, + ) +end + +@testset "Nested resource canonicalization" begin + child = Dict( + "\$id" => "sub/", + "\$defs" => Dict("value" => Dict("\$ref" => "other")), + ) + parent = Dict( + "\$id" => "https://example.com/parent/", + "\$defs" => Dict("child" => child), + ) + schema = Dict( + "\$schema" => SchemaEngine.DRAFT202012.uri, + "\$id" => "https://example.com/root", + "\$defs" => Dict("parent" => parent), + "\$ref" => "https://example.com/parent/#/\$defs/child/\$defs/value", + ) + retriever = Resources.MemoryRetriever( + Dict( + "https://example.com/parent/sub/other" => "{\"type\":\"integer\"}", + ), + ) + compiled = SchemaEngine.CompiledSchema(schema; retriever) + @test isvalid(compiled, 1) + @test !isvalid(compiled, "text") +end + +@testset "Embedded schema resources" begin + document_id = Resources.ResourceId("https://example.com/api.json") + document = Resources.Resource( + document_id, + Dict( + "components" => Dict( + "schemas" => Dict( + "Root" => Dict("\$ref" => "#/components/schemas/Value"), + "Value" => Dict("type" => "string"), + ), + ), + ), + ) + compiled = SchemaEngine.CompiledSchema( + document, + Resources.JSONPointer("/components/schemas/Root"); + dialect = SchemaEngine.DRAFT202012, + ) + @test isvalid(compiled, "text") + @test !isvalid(compiled, 1) + @test compiled.registry isa Resources.FrozenRegistry + resources = compiled.registry.resources + empty!(resources) + dialects = compiled.dialects + empty!(dialects) + @test isvalid(compiled, "still immutable") + + self_id = Resources.ResourceId("https://example.com/self") + self_resource = Resources.Resource( + self_id, + Dict("\$id" => string(self_id), "type" => "integer"), + ) + self_compiled = SchemaEngine.CompiledSchema( + self_resource; + dialect = SchemaEngine.DRAFT202012, + ) + @test self_compiled.root.resource == self_id + @test isvalid(self_compiled, 1) + + declared_id = Resources.ResourceId("https://example.com/declared") + declared_resource = Resources.Resource( + Resources.ResourceId("https://example.com/retrieved"), + Dict("\$id" => string(declared_id), "type" => "string"), + ) + declared_compiled = SchemaEngine.CompiledSchema( + declared_resource; + dialect = SchemaEngine.DRAFT202012, + ) + @test declared_compiled.root.resource == declared_id + @test isvalid(declared_compiled, "text") + + nested_document_id = + Resources.ResourceId("https://example.com/outer/root.json") + nested_document = Resources.Resource( + nested_document_id, + Dict( + "\$defs" => Dict( + "inner" => Dict( + "\$id" => "inner/", + "\$defs" => Dict("leaf" => Dict("\$ref" => "other")), + ), + ), + ), + ) + nested_retriever = Resources.MemoryRetriever( + Dict( + "https://example.com/outer/inner/other" => "{\"type\":\"integer\"}", + "https://example.com/outer/other" => "{\"type\":\"string\"}", + ), + ) + nested_compiled = SchemaEngine.CompiledSchema( + nested_document, + Resources.JSONPointer("/\$defs/inner/\$defs/leaf"); + dialect = SchemaEngine.DRAFT202012, + retriever = nested_retriever, + ) + @test isvalid(nested_compiled, 1) + @test !isvalid(nested_compiled, "text") +end + +@testset "Multiple embedded schema roots" begin + document_id = Resources.ResourceId("https://example.com/api.json") + document = Resources.Resource( + document_id, + Dict( + "components" => Dict( + "schemas" => Dict( + # Keep the referring schema first. Compilation must not + # depend on object or root order. + "Result" => Dict( + "\$id" => "result", + "type" => "object", + "properties" => Dict( + "value" => Dict("\$ref" => "value"), + ), + "required" => ["value"], + ), + "Value" => Dict( + "\$id" => "value", + "type" => "string", + "minLength" => 2, + ), + "Pointer" => Dict("\$ref" => "#/components/schemas/Value"), + ), + ), + ), + ) + result_pointer = Resources.JSONPointer("/components/schemas/Result") + value_pointer = Resources.JSONPointer("/components/schemas/Value") + pointer_pointer = Resources.JSONPointer("/components/schemas/Pointer") + roots = [result_pointer, value_pointer, pointer_pointer] + schemas = SchemaEngine.CompiledSchemas( + document, + roots; + dialect = SchemaEngine.DRAFT202012, + ) + + result = SchemaEngine.select( + schemas, + Resources.NodeId(document_id, result_pointer), + ) + value = SchemaEngine.select(schemas, document_id, value_pointer) + pointer = SchemaEngine.select(schemas, document_id, pointer_pointer) + @test isvalid(result, Dict("value" => "ok")) + @test !isvalid(result, Dict("value" => "x")) + @test isvalid(value, "ok") + @test !isvalid(value, 1) + @test isvalid(pointer, "ok") + @test !isvalid(pointer, 1) + value_property = SchemaEngine.subschema( + schemas, + Resources.ResourceId("https://example.com/result"), + Resources.JSONPointer("/properties/value"), + ) + @test isvalid(value_property, "ok") + @test !isvalid(value_property, 1) + result_value_node = Resources.NodeId( + Resources.ResourceId("https://example.com/result"), + Resources.JSONPointer("/properties/value"), + ) + expected_value_node = Resources.NodeId( + Resources.ResourceId("https://example.com/value"), + Resources.JSONPointer(), + ) + @test SchemaEngine.reference_target(schemas, result_value_node) == + expected_value_node + @test SchemaEngine.reference_target( + schemas, + Resources.NodeId(document_id, value_pointer), + ) === nothing + @test SchemaEngine.reference_target(result, result_value_node) == + expected_value_node + + roots_copy = schemas.roots + empty!(roots_copy) + @test isvalid(value, "still immutable") + @test_throws ArgumentError SchemaEngine.select( + schemas, + document_id, + Resources.JSONPointer("/components/schemas/Missing"), + ) + + external_id = Resources.ResourceId("https://example.net/shared.json") + external = Resources.Resource( + external_id, + Dict("schemas" => Dict("Count" => Dict("type" => "integer"))), + ) + external_pointer = Resources.JSONPointer("/schemas/Count") + combined = SchemaEngine.CompiledSchemas( + [document, external], + [ + Resources.NodeId(document_id, value_pointer), + Resources.NodeId(external_id, external_pointer), + ]; + dialect = SchemaEngine.DRAFT202012, + ) + count = SchemaEngine.select(combined, external_id, external_pointer) + @test isvalid(count, 1) + @test !isvalid(count, "one") + + dialect_document_id = Resources.ResourceId("https://example.org/mixed.json") + legacy_pointer = Resources.JSONPointer("/schemas/Legacy") + modern_pointer = Resources.JSONPointer("/schemas/Modern") + dialect_document = Resources.Resource( + dialect_document_id, + Dict( + "schemas" => Dict( + "Legacy" => Dict( + "id" => "legacy", + "type" => "number", + "minimum" => 0, + "exclusiveMinimum" => true, + ), + "Modern" => Dict( + "\$id" => "modern", + "type" => "number", + "exclusiveMinimum" => 0, + ), + ), + ), + ) + legacy_node = Resources.NodeId(dialect_document_id, legacy_pointer) + modern_node = Resources.NodeId(dialect_document_id, modern_pointer) + mixed = SchemaEngine.CompiledSchemas( + [dialect_document], + [legacy_node, modern_node]; + dialect = SchemaEngine.DRAFT7, + root_dialects = Dict( + legacy_node => SchemaEngine.DRAFT4, + modern_node => SchemaEngine.DRAFT202012, + ), + ) + legacy = SchemaEngine.select(mixed, legacy_node) + modern = SchemaEngine.select(mixed, modern_node) + @test legacy.dialect === SchemaEngine.DRAFT4 + @test modern.dialect === SchemaEngine.DRAFT202012 + @test !isvalid(legacy, 0) + @test isvalid(legacy, 1) + @test !isvalid(modern, 0) + @test isvalid(modern, 1) + + application_dialect = "https://example.org/dialect/application" + aliased = SchemaEngine.CompiledSchema( + Dict( + "\$schema" => application_dialect, + "type" => "integer", + "minimum" => 1, + ); + dialect_aliases = Dict(application_dialect => :draft202012), + ) + @test aliased.dialect === SchemaEngine.DRAFT202012 + @test isvalid(aliased, 1) + @test !isvalid(aliased, 0) + aliases = aliased.dialect_aliases + empty!(aliases) + @test haskey(aliased.dialect_aliases, application_dialect) + + aliased_resource_id = Resources.ResourceId("urn:application:schema") + aliased_resource = Resources.Resource( + aliased_resource_id, + Dict( + "\$schema" => application_dialect, + "type" => "string", + "minLength" => 2, + ), + ) + aliased_graph = SchemaEngine.CompiledSchemas( + [aliased_resource], + [Resources.NodeId(aliased_resource_id, Resources.JSONPointer())]; + dialect_aliases = Dict(application_dialect => SchemaEngine.DRAFT202012), + ) + aliased_root = SchemaEngine.select(aliased_graph, aliased_resource_id) + @test isvalid(aliased_root, "ok") + @test !isvalid(aliased_root, "x") + @test_throws ArgumentError SchemaEngine.CompiledSchema( + Dict("type" => "integer"); + dialect_aliases = Dict(1 => SchemaEngine.DRAFT202012), + ) + + @test_throws ArgumentError SchemaEngine.CompiledSchemas( + Resources.Resource[], + Resources.NodeId[]; + dialect = SchemaEngine.DRAFT202012, + ) + @test_throws ArgumentError SchemaEngine.CompiledSchemas( + [document], + Resources.NodeId[]; + dialect = SchemaEngine.DRAFT202012, + ) + @test_throws ArgumentError SchemaEngine.CompiledSchemas( + [document, external], + [Resources.NodeId(document_id, value_pointer)]; + dialect = SchemaEngine.DRAFT202012, + max_resources = 1, + ) +end + +@testset "Compiled evaluation concurrency" begin + schema = Dict{String,Any}("type" => "integer") + instance = 1 + for _ in 1:60 + schema = Dict{String,Any}( + "type" => "object", + "properties" => Dict("value" => schema), + "required" => ["value"], + ) + instance = Dict{String,Any}("value" => instance) + end + compiled = SchemaEngine.CompiledSchema(schema; dialect = SchemaEngine.DRAFT7) + @test isvalid(compiled, instance) + @test all( + fetch, + [Threads.@spawn(isvalid(compiled, instance)) for _ in 1:32], + ) +end + +@testset "Compiler resource and vocabulary limits" begin + definitions = Dict( + "resource-$index" => Dict( + "\$id" => "https://example.com/resource-$index", + "type" => "integer", + ) for index in 1:10 + ) + @test_throws SchemaEngine.CompilationError SchemaEngine.CompiledSchema( + Dict("\$schema" => SchemaEngine.DRAFT202012.uri, "\$defs" => definitions); + max_resources = 5, + ) + + dialect_uri = "https://example.com/format-dialect" + meta = JSON.json( + Dict( + "\$schema" => SchemaEngine.DRAFT202012.uri, + "\$id" => dialect_uri, + "\$vocabulary" => Dict( + "https://json-schema.org/draft/2020-12/vocab/core" => true, + "https://json-schema.org/draft/2020-12/vocab/format-assertion" => + true, + ), + ), + ) + retriever = Resources.MemoryRetriever(Dict(dialect_uri => meta)) + @test_throws SchemaEngine.CompilationError SchemaEngine.CompiledSchema( + Dict("\$schema" => dialect_uri); + dialect = SchemaEngine.DRAFT202012, + retriever, + ) +end diff --git a/test/schema_engine/official.jl b/test/schema_engine/official.jl new file mode 100644 index 0000000..1601945 --- /dev/null +++ b/test/schema_engine/official.jl @@ -0,0 +1,83 @@ +import ZipFile + +const OPENAPI_SCHEMA_SUITE_REVISION = "be54236db6e8e6bb2e098ed16fb4c61e73f5a9ac" +const OPENAPI_SCHEMA_SUITE_URL = + "https://github.com/json-schema-org/JSON-Schema-Test-Suite/archive/$(OPENAPI_SCHEMA_SUITE_REVISION).zip" + +function _official_schema_suite_dir() + directory = mktempdir() + archive = joinpath(directory, "suite.zip") + destination = joinpath(directory, "suite") + mkpath(destination) + Downloads.download(OPENAPI_SCHEMA_SUITE_URL, archive) + reader = ZipFile.Reader(archive) + try + for entry in reader.files + relative = normpath(entry.name) + (relative == ".." || startswith(relative, ".." * Base.Filesystem.path_separator)) && + error("schema suite archive contains an unsafe path") + target = joinpath(destination, relative) + if endswith(entry.name, "/") + mkpath(target) + else + mkpath(dirname(target)) + write(target, read(entry)) + end + end + finally + close(reader) + end + return joinpath( + destination, + "JSON-Schema-Test-Suite-$(OPENAPI_SCHEMA_SUITE_REVISION)", + "tests", + ) +end + +function _test_compiled_draft(directory, retriever, schema_dialect, base_uri) + files = sort(filter(name -> endswith(name, ".json"), readdir(directory))) + @testset "$(file)" for file in files + groups = JSON.parsefile(joinpath(directory, file)) + @testset "$(group["description"])" for group in groups + compiled = SchemaEngine.CompiledSchema( + group["schema"]; + dialect = schema_dialect, + base_uri, + retriever, + ) + @testset "$(case["description"])" for case in group["tests"] + valid = case["valid"] + @test isvalid(compiled, case["data"]) == valid + @test isempty( + SchemaEngine.validate( + compiled, + case["data"]; + fail_fast = false, + ), + ) == valid + end + end + end +end + +@testset "Official JSON Schema suite" begin + suite = _official_schema_suite_dir() + remotes = normpath(suite, "..", "remotes") + retriever = SuiteRetriever(remotes) + for (name, schema_dialect) in ( + "draft2020-12" => SchemaEngine.DRAFT202012, + "draft2019-09" => SchemaEngine.DRAFT201909, + "draft7" => SchemaEngine.DRAFT7, + "draft6" => SchemaEngine.DRAFT6, + "draft4" => SchemaEngine.DRAFT4, + ) + @testset "$name" begin + _test_compiled_draft( + joinpath(suite, name), + retriever, + schema_dialect, + "http://localhost:1234/$name/root", + ) + end + end +end diff --git a/test/schema_engine/rebase.jl b/test/schema_engine/rebase.jl new file mode 100644 index 0000000..7c42a0e --- /dev/null +++ b/test/schema_engine/rebase.jl @@ -0,0 +1,103 @@ +@testset "Compiled schema graph rebasing" begin + R = SchemaEngine.Resources + root_id = R.ResourceId("file:///private/build/openapi.json") + external_id = R.ResourceId("file:///private/build/common.json") + root = R.Resource( + root_id, + Dict( + "schemas" => Dict( + "Node" => Dict( + "\$id" => "models/node.json", + "\$dynamicAnchor" => "node", + "type" => "object", + "required" => ["value"], + "properties" => Dict( + "value" => Dict("type" => "string"), + "child" => Dict("\$dynamicRef" => "#node"), + ), + "additionalProperties" => false, + ), + "Count" => Dict( + "\$ref" => "./common.json#/\$defs/Count", + ), + ), + ), + ) + external = R.Resource( + external_id, + Dict("\$defs" => Dict("Count" => Dict("type" => "integer"))), + ) + node_root = R.NodeId(root_id, R.JSONPointer("/schemas/Node")) + count_root = R.NodeId(root_id, R.JSONPointer("/schemas/Count")) + graph = SchemaEngine.CompiledSchemas( + [root, external], + [node_root, count_root]; + dialect = SchemaEngine.DRAFT202012, + ) + ids = sort( + collect(keys(getfield(graph.template.registry, :resources))); + by = string, + ) + mapping = Dict( + id => R.ResourceId("https://portable.invalid/schema/$index.json") for + (index, id) in enumerate(ids) + ) + rebased = SchemaEngine.rebase(graph, mapping) + + samples = Any[ + Dict("value" => "root"), + Dict("value" => "root", "child" => Dict("value" => "leaf")), + Dict("value" => "root", "child" => Dict("value" => 1)), + ] + original_node = SchemaEngine.select(graph, node_root) + rebased_node = SchemaEngine.select(rebased, node_root) + @test [isvalid(original_node, value) for value in samples] == + [isvalid(rebased_node, value) for value in samples] + @test isvalid(SchemaEngine.select(rebased, count_root), 3) + @test !isvalid(SchemaEngine.select(rebased, count_root), "3") + @test SchemaEngine.subschema(graph.template, original_node.root).root == + original_node.root + + serialized = join( + JSON.json(resource.contents) * string(resource.id) for + resource in values(getfield(rebased.template.registry, :resources)) + ) + @test !occursin("file:///private/build", serialized) + @test occursin("https://portable.invalid/schema/", serialized) + + recursive_id = R.ResourceId("file:///private/build/recursive.json") + recursive_root = R.NodeId(recursive_id, R.JSONPointer()) + recursive_resource = R.Resource( + recursive_id, + Dict( + "\$recursiveAnchor" => true, + "type" => "object", + "properties" => Dict( + "child" => Dict("\$recursiveRef" => "#"), + ), + "additionalProperties" => false, + ), + ) + recursive = SchemaEngine.CompiledSchemas( + [recursive_resource], + [recursive_root]; + dialect = SchemaEngine.DRAFT201909, + ) + recursive_mapping = Dict( + only(keys(getfield(recursive.template.registry, :resources))) => + R.ResourceId("https://portable.invalid/recursive.json"), + ) + portable_recursive = SchemaEngine.rebase(recursive, recursive_mapping) + recursive_samples = Any[ + Dict(), + Dict("child" => Dict("child" => Dict())), + Dict("child" => 1), + ] + @test [ + isvalid(SchemaEngine.select(recursive, recursive_root), value) for + value in recursive_samples + ] == [ + isvalid(SchemaEngine.select(portable_recursive, recursive_root), value) for + value in recursive_samples + ] +end diff --git a/test/schema_engine/resources.jl b/test/schema_engine/resources.jl new file mode 100644 index 0000000..ce174ad --- /dev/null +++ b/test/schema_engine/resources.jl @@ -0,0 +1,215 @@ +const Resources = SchemaEngine.Resources + +@testset "JSON Pointer" begin + pointer = Resources.JSONPointer("/a~1b/m~0n//0") + @test collect(pointer) == ["a/b", "m~n", "", "0"] + @test string(pointer) == "/a~1b/m~0n//0" + @test string(Resources.JSONPointer() / "a/b" / "m~n") == "/a~1b/m~0n" + @test isempty(Resources.JSONPointer("")) + @test_throws Resources.PointerError Resources.JSONPointer("a") + @test_throws Resources.PointerError Resources.JSONPointer("/~") + @test_throws Resources.PointerError Resources.JSONPointer("/~2") + + document = Dict( + "a/b" => Dict("m~n" => 1), + "array" => Any["zero", Dict("" => "empty")], + ) + @test Resources.resolve(document, Resources.JSONPointer("/a~1b/m~0n")) == 1 + @test Resources.resolve(document, Resources.JSONPointer("/array/0")) == + "zero" + @test Resources.resolve(document, Resources.JSONPointer("/array/1/")) == + "empty" + @test collect(Resources.JSONPointer("/")) == [""] + @test_throws Resources.PointerError Resources.resolve( + document, + Resources.JSONPointer("/array/01"), + ) + @test_throws Resources.PointerError Resources.resolve( + document, + Resources.JSONPointer("/array/+1"), + ) + @test_throws Resources.PointerError Resources.resolve( + document, + Resources.JSONPointer("/array/ 1"), + ) + @test_throws Resources.PointerError Resources.resolve( + document, + Resources.JSONPointer("/array/-"), + ) + @test_throws Resources.PointerError Resources.resolve( + document, + Resources.JSONPointer("/array/2"), + ) + @test_throws Resources.PointerError Resources.resolve( + document, + Resources.JSONPointer("/missing"), + ) +end + +@testset "Frozen JSON values" begin + source = Dict("object" => Dict("value" => 1), "array" => Any[true, nothing]) + frozen = Resources.freeze(source) + source["object"]["value"] = 2 + push!(source["array"], false) + @test frozen isa Resources.FrozenObject + @test frozen["object"] isa Resources.FrozenObject + @test frozen["array"] isa Resources.FrozenArray + @test frozen["object"]["value"] == 1 + @test frozen["array"] == Any[true, nothing] + @test_throws MethodError setindex!(frozen, 2, "object") + @test_throws Base.CanonicalIndexError setindex!(frozen["array"], false, 1) + @test Resources.freeze(frozen) === frozen + @test_throws ArgumentError Resources.freeze(Dict(1 => "invalid")) +end + +@testset "Resource identifiers and references" begin + id = Resources.ResourceId("HTTPS://EXAMPLE.COM:443/schemas/root.json") + @test string(id) == "https://example.com/schemas/root.json" + @test_throws ArgumentError Resources.ResourceId( + "https://example.com/root#part", + ) + + reference = Resources.Reference(id, "../common.json#/a%20b/~0value") + @test string(reference.resource) == "https://example.com/common.json" + @test reference.fragment isa Resources.PointerFragment + @test collect(reference.fragment.pointer) == ["a b", "~value"] + + anchor = Resources.Reference(id, "#named%2Danchor") + @test anchor.fragment == Resources.AnchorFragment("named-anchor") + @test Resources.Reference(id, "#").fragment isa Resources.RootFragment + @test Resources.ResourceId("https://EXAMPLE.com:443/a/../b/%7euser") == + Resources.ResourceId("https://example.com/b/~user") + @test Resources.ResourceId("https://example.com") == + Resources.ResourceId("https://example.com/") + @test Resources.ResourceId("https://example.com/%2f") == + Resources.ResourceId("https://example.com/%2F") + @test string(Resources.ResourceId("urn:openapi:inline")) == + "urn:openapi:inline" + @test string(Resources.ResourceId("MAILTO:user@example.com")) == + "mailto:user@example.com" + @test string(Resources.ResourceId("custom://EXAMPLE.com/schema")) == + "custom://example.com/schema" + @test string(Resources.ResourceId("https://[::1]:443/schema")) == + "https://[::1]/schema" + @test Resources.ResourceId( + string(Resources.ResourceId("urn:test:a%2fb")), + ) == Resources.ResourceId("urn:test:a%2Fb") +end + +@testset "Resource registry" begin + retrieval = Resources.ResourceId("file:///tmp/schema.json") + canonical = Resources.ResourceId("https://example.com/schema") + document = Dict("defs" => Dict("thing" => Dict("type" => "string"))) + item_pointer = Resources.JSONPointer("/defs/thing") + item = Resources.Resource( + canonical, + document; + retrieval, + media_type = "application/schema+json", + ) + registry = Resources.Registry() + @test isempty(registry) + Resources.register!(registry, item; anchors = ["thing" => item_pointer]) + @test length(registry) == 1 + @test !isempty(Resources.freeze(registry)) + + by_pointer = Resources.resolve( + registry, + Resources.Reference(retrieval, "#/defs/thing"), + ) + @test by_pointer.id == Resources.NodeId(canonical, item_pointer) + @test by_pointer.value["type"] == "string" + + by_anchor = + Resources.resolve(registry, Resources.Reference(canonical, "#thing")) + @test by_anchor.id == by_pointer.id + @test by_anchor.value === by_pointer.value + dynamic = Resources.register_anchor!( + registry, + canonical, + "dynamic", + item_pointer; + dynamic = true, + ) + @test registry.dynamic_anchors[(canonical, "dynamic")] == dynamic + @test_throws Resources.MissingAnchorError Resources.resolve( + registry, + Resources.Reference(canonical, "#missing"), + ) + @test_throws Resources.MissingResourceError Resources.resource( + registry, + Resources.ResourceId("https://example.com/missing"), + ) + @test_throws Resources.DuplicateResourceError Resources.register!( + registry, + item, + ) + + invalid_registry = Resources.Registry() + @test_throws Resources.PointerError Resources.register!( + invalid_registry, + item; + anchors = ["invalid" => Resources.JSONPointer("/missing")], + ) + @test isempty(invalid_registry.resources) + @test isempty(invalid_registry.aliases) + @test isempty(invalid_registry.anchors) + @test isempty(invalid_registry.dynamic_anchors) +end + +@testset "Bounded resource retrieval" begin + id = Resources.ResourceId("https://example.com/schema.json") + memory = + Resources.MemoryRetriever(Dict(string(id) => "{\"type\":\"string\"}")) + retrieved = Resources.retrieve(memory, id) + @test String(copy(retrieved.bytes)) == "{\"type\":\"string\"}" + retrieved.bytes[1] = UInt8('!') + @test String(copy(Resources.retrieve(memory, id).bytes)) == + "{\"type\":\"string\"}" + stored = Resources.RetrievedResource(id, Vector{UInt8}(codeunits("{}"))) + aliased = Resources.MemoryRetriever(Dict(id => stored)) + stored.bytes[1] = UInt8('!') + @test String(Resources.retrieve(aliased, id).bytes) == "{}" + @test_throws Resources.RetrievalError Resources.retrieve( + Resources.DisabledRetriever(), + id, + ) + + root = mktempdir() + file = joinpath(root, "schema.json") + write(file, "{}") + file_id = Resources.ResourceId("file://" * file) + @test String( + copy(Resources.retrieve(Resources.FileRetriever(root), file_id).bytes), + ) == "{}" + @test_throws Resources.RetrievalError Resources.retrieve( + Resources.FileRetriever(root; max_bytes = 1), + file_id, + ) + outside = tempname() + write(outside, "{}") + outside_id = Resources.ResourceId("file://" * outside) + @test_throws Resources.RetrievalError Resources.retrieve( + Resources.FileRetriever(root), + outside_id, + ) + if Sys.isunix() + link = joinpath(root, "linked-schema.json") + symlink(outside, link) + @test_throws Resources.RetrievalError Resources.retrieve( + Resources.FileRetriever(root), + Resources.ResourceId("file://" * link), + ) + end + missing_id = + Resources.ResourceId("file://" * joinpath(root, "missing.json")) + @test_throws Resources.RetrievalError Resources.retrieve( + Resources.FileRetriever(root), + missing_id, + ) + directory_id = Resources.ResourceId("file://" * root) + @test_throws Resources.RetrievalError Resources.retrieve( + Resources.FileRetriever(root), + directory_id, + ) +end diff --git a/test/semantics.jl b/test/semantics.jl new file mode 100644 index 0000000..e757673 --- /dev/null +++ b/test/semantics.jl @@ -0,0 +1,445 @@ +semantic_empty_response() = OpenAPI.obj( + "204" => OpenAPI.obj("description" => "empty"), +) + +function semantic_operation(id; responses = semantic_empty_response(), kwargs...) + operation = OpenAPI.obj("operationId" => String(id)) + responses === nothing || (operation["responses"] = responses) + for (key, value) in kwargs + operation[String(key)] = value + end + return operation +end + +@testset "OpenAPI semantic coverage" begin + @testset "OAS 3.2 methods and explicit generation deferrals" begin + paths = OpenAPI.obj( + "/items" => OpenAPI.obj( + "query" => semantic_operation("queryItems"; responses = nothing), + "additionalOperations" => OpenAPI.obj( + "PURGE" => semantic_operation("purgeItems"; responses = nothing), + ), + ), + "/search" => OpenAPI.obj( + "get" => semantic_operation( + "search"; + parameters = Any[ + OpenAPI.obj( + "name" => "query", + "in" => "querystring", + "required" => true, + "content" => OpenAPI.obj( + "application/x-www-form-urlencoded" => OpenAPI.obj( + "schema" => OpenAPI.obj("type" => "string"), + ), + ), + ), + ], + ), + ), + "/events" => OpenAPI.obj( + "get" => semantic_operation( + "events"; + responses = OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "events", + "content" => OpenAPI.obj( + "application/jsonl" => OpenAPI.obj( + "itemSchema" => OpenAPI.obj( + "type" => "object", + ), + ), + ), + ), + ), + ), + ), + ) + document = minimal_openapi("3.2.0", paths) + api = OpenAPI.normalize(document) + methods = Dict(operation.id => operation.method for operation in api.operations) + @test methods["queryItems"] === :QUERY + @test methods["purgeItems"] === :PURGE + @test isempty(only(filter(operation -> operation.id == "queryItems", api.operations)).responses) + + error = @test_throws OpenAPI.OpenAPIError OpenAPI.plan(api) + codes = Set(diagnostic.code for diagnostic in error.value.diagnostics) + @test :unsupported_querystring_generation in codes + @test :unsupported_streaming_generation in codes + end + + @testset "OAS 3.2 Media Type references and response headers" begin + document = minimal_openapi( + "3.2.0", + OpenAPI.obj( + "/x" => OpenAPI.obj( + "get" => semantic_operation( + "getX"; + responses = OpenAPI.obj( + "200" => OpenAPI.obj( + "summary" => "success", + "headers" => OpenAPI.obj( + "Content-Type" => OpenAPI.obj( + "schema" => OpenAPI.obj("type" => "string"), + ), + "X-Rate" => OpenAPI.obj( + "required" => true, + "schema" => OpenAPI.obj("type" => "integer"), + ), + ), + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "\$ref" => "#/components/mediaTypes/JsonValue", + ), + ), + ), + ), + ), + ), + ), + ) + document["components"] = OpenAPI.obj( + "mediaTypes" => OpenAPI.obj( + "JsonValue" => OpenAPI.obj( + "schema" => OpenAPI.obj("type" => "string"), + ), + ), + ) + api = OpenAPI.normalize(document) + response = only(only(api.operations).responses) + @test response.summary == "success" + @test response.description === nothing + @test length(response.content) == 1 + @test response.content[1].schema !== nothing + @test only(response.headers).name == "X-Rate" + @test only(response.headers).required + @test any( + diagnostic -> diagnostic.code === :ignored_content_type_header, + api.diagnostics, + ) + end + + @testset "reserved request headers are ignored" begin + operation = semantic_operation( + "headers"; + parameters = Any[ + OpenAPI.obj( + "name" => name, + "in" => "header", + "schema" => OpenAPI.obj("type" => "string"), + ) for name in ("Accept", "Content-Type", "Authorization") + ], + ) + api = OpenAPI.normalize( + minimal_openapi( + "3.1.1", + OpenAPI.obj("/headers" => OpenAPI.obj("get" => operation)), + ), + ) + @test isempty(only(api.operations).parameters) + @test count( + diagnostic -> diagnostic.code === :ignored_header_parameter, + api.diagnostics, + ) == 3 + end + + @testset "semantic conflicts collect stable diagnostics" begin + document = minimal_openapi( + "3.1.1", + OpenAPI.obj( + "/pets/{id}" => OpenAPI.obj( + "get" => semantic_operation( + "duplicate"; + parameters = Any[ + OpenAPI.obj( + "name" => "id", + "in" => "path", + "required" => true, + "schema" => OpenAPI.obj("type" => "string"), + ), + ], + ), + ), + "/pets/{name}" => OpenAPI.obj( + "get" => semantic_operation( + "duplicate"; + parameters = Any[ + OpenAPI.obj( + "name" => "name", + "in" => "path", + "required" => true, + "schema" => OpenAPI.obj("type" => "string"), + ), + ], + ), + ), + ), + ) + error = @test_throws OpenAPI.OpenAPIError OpenAPI.normalize(document) + codes = Set(diagnostic.code for diagnostic in error.value.diagnostics) + @test :ambiguous_path_template in codes + @test :duplicate_operation_id in codes + + compatible = minimal_openapi( + "3.1.1", + OpenAPI.obj( + "/pets/{id}" => OpenAPI.obj( + "get" => semantic_operation( + "byId"; + parameters = Any[ + OpenAPI.obj( + "name" => "id", + "in" => "path", + "required" => true, + "schema" => OpenAPI.obj("type" => "string"), + ), + ], + ), + ), + "/pets/{name}" => OpenAPI.obj( + "get" => semantic_operation( + "byName"; + parameters = Any[ + OpenAPI.obj( + "name" => "name", + "in" => "path", + "required" => true, + "schema" => OpenAPI.obj("type" => "string"), + ), + ], + ), + ), + ), + ) + permissive = OpenAPI.normalize(compatible; strict = false) + @test length(permissive.operations) == 2 + warning = only( + diagnostic for diagnostic in permissive.diagnostics if + diagnostic.code === :ambiguous_path_template + ) + @test warning.severity === :warning + plan = OpenAPI.plan(permissive; strict = false) + @test any( + diagnostic -> diagnostic.code === :ambiguous_path_template, + plan.diagnostics, + ) + + media = minimal_openapi( + "3.1.1", + OpenAPI.obj( + "/media" => OpenAPI.obj( + "get" => semantic_operation( + "media"; + responses = OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "media", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj(), + "Application/JSON; charset=utf-8" => OpenAPI.obj(), + "not a media type" => OpenAPI.obj(), + ), + ), + ), + ), + ), + ), + ) + media_error = @test_throws OpenAPI.OpenAPIError OpenAPI.normalize(media) + media_codes = Set(diagnostic.code for diagnostic in media_error.value.diagnostics) + @test :duplicate_media_type in media_codes + @test :invalid_media_type in media_codes + end + + @testset "planning rejects incompatible styles and encodings" begin + document = minimal_openapi( + "3.1.1", + OpenAPI.obj( + "/invalid" => OpenAPI.obj( + "post" => semantic_operation( + "invalid"; + parameters = Any[ + OpenAPI.obj( + "name" => "filter", + "in" => "query", + "style" => "deepObject", + "explode" => true, + "schema" => OpenAPI.obj("type" => "string"), + ), + OpenAPI.obj( + "name" => "values", + "in" => "query", + "style" => "pipeDelimited", + "explode" => false, + "schema" => OpenAPI.obj("type" => "string"), + ), + ], + requestBody = OpenAPI.obj( + "content" => OpenAPI.obj( + "multipart/mixed" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "type" => "object", + "properties" => OpenAPI.obj( + "known" => OpenAPI.obj("type" => "string"), + ), + "additionalProperties" => false, + ), + "encoding" => OpenAPI.obj( + "unknown" => OpenAPI.obj( + "contentType" => "text/plain", + ), + ), + ), + ), + ), + ), + ), + ), + ) + error = @test_throws OpenAPI.OpenAPIError OpenAPI.plan(document) + codes = Set(diagnostic.code for diagnostic in error.value.diagnostics) + @test :invalid_deep_object_schema in codes + @test :invalid_delimited_schema in codes + @test :ignored_encoding_property in codes + ignored = only( + diagnostic for diagnostic in error.value.diagnostics if + diagnostic.code === :ignored_encoding_property + ) + @test ignored.severity === :warning + end + + @testset "permissive vendor serialization compatibility" begin + document = minimal_openapi( + "3.0.4", + OpenAPI.obj( + "/items" => OpenAPI.obj( + "get" => semantic_operation( + "items"; + parameters = Any[ + OpenAPI.obj( + "name" => "expand", + "in" => "query", + "style" => "deepObject", + "explode" => true, + "schema" => OpenAPI.obj( + "type" => "array", + "items" => OpenAPI.obj("type" => "string"), + ), + ), + ], + ), + ), + ), + ) + @test_throws OpenAPI.OpenAPIError OpenAPI.plan(document) + plan = OpenAPI.plan(document; strict = false) + warning = only( + diagnostic for diagnostic in plan.diagnostics if + diagnostic.code === :invalid_deep_object_schema + ) + @test warning.severity === :warning + @test occursin("non-standard", warning.message) + end + + @testset "encoding fields follow their media-type scope" begin + document = minimal_openapi( + "3.1.1", + OpenAPI.obj( + "/upload" => OpenAPI.obj( + "post" => semantic_operation( + "upload"; + requestBody = OpenAPI.obj( + "content" => OpenAPI.obj( + "multipart/mixed" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "type" => "object", + "properties" => OpenAPI.obj( + "file" => OpenAPI.obj("type" => "string"), + ), + ), + "encoding" => OpenAPI.obj( + "file" => OpenAPI.obj( + "style" => "form", + "explode" => true, + "allowReserved" => true, + "headers" => OpenAPI.obj( + "X-Part" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "type" => "integer", + ), + ), + ), + ), + ), + ), + "application/x-www-form-urlencoded" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "type" => "object", + "properties" => OpenAPI.obj( + "file" => OpenAPI.obj("type" => "string"), + ), + ), + "encoding" => OpenAPI.obj( + "file" => OpenAPI.obj( + "headers" => OpenAPI.obj( + "X-Ignored" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "type" => "string", + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ) + api = OpenAPI.normalize(document) + codes = Set(diagnostic.code for diagnostic in api.diagnostics) + @test :ignored_non_form_multipart_encoding_style in codes + @test :ignored_form_encoding_headers in codes + + plan = OpenAPI.plan(api; name = "EncodingScopeClient") + source = OpenAPI.client(plan) + @test occursin("multipart_headers = NamedTuple()", source) + @test occursin("name = \"X-Part\"", source) + @test !occursin("name = \"X-Ignored\"", source) + end + + @testset "security scopes and server names" begin + document = minimal_openapi( + "3.2.0", + OpenAPI.obj( + "/secure" => OpenAPI.obj( + "get" => semantic_operation( + "secure"; + security = Any[OpenAPI.obj("OAuth" => ["missing"])], + ), + ), + ), + ) + document["servers"] = Any[ + OpenAPI.obj("name" => "same", "url" => "https://one.example"), + OpenAPI.obj("name" => "same", "url" => "https://two.example"), + ] + document["components"] = OpenAPI.obj( + "securitySchemes" => OpenAPI.obj( + "OAuth" => OpenAPI.obj( + "type" => "oauth2", + "flows" => OpenAPI.obj( + "clientCredentials" => OpenAPI.obj( + "tokenUrl" => "https://auth.example/token", + "scopes" => OpenAPI.obj("read" => "read"), + ), + ), + ), + ), + ) + error = @test_throws OpenAPI.OpenAPIError OpenAPI.normalize(document) + codes = Set(diagnostic.code for diagnostic in error.value.diagnostics) + @test :unknown_oauth_scope in codes + @test :duplicate_server_name in codes + end +end diff --git a/test/server/allany/AllAnyServer/.openapi-generator-ignore b/test/server/allany/AllAnyServer/.openapi-generator-ignore deleted file mode 100644 index 7484ee5..0000000 --- a/test/server/allany/AllAnyServer/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/test/server/allany/AllAnyServer/.openapi-generator/FILES b/test/server/allany/AllAnyServer/.openapi-generator/FILES deleted file mode 100644 index 053d86e..0000000 --- a/test/server/allany/AllAnyServer/.openapi-generator/FILES +++ /dev/null @@ -1,25 +0,0 @@ -README.md -docs/AnyOfBaseType.md -docs/AnyOfMappedPets.md -docs/AnyOfPets.md -docs/Cat.md -docs/DefaultApi.md -docs/Dog.md -docs/OneOfBaseType.md -docs/OneOfMappedPets.md -docs/OneOfPets.md -docs/Pet.md -docs/TypeWithAllArrayTypes.md -src/AllAnyServer.jl -src/apis/api_DefaultApi.jl -src/modelincludes.jl -src/models/model_AnyOfBaseType.jl -src/models/model_AnyOfMappedPets.jl -src/models/model_AnyOfPets.jl -src/models/model_Cat.jl -src/models/model_Dog.jl -src/models/model_OneOfBaseType.jl -src/models/model_OneOfMappedPets.jl -src/models/model_OneOfPets.jl -src/models/model_Pet.jl -src/models/model_TypeWithAllArrayTypes.jl diff --git a/test/server/allany/AllAnyServer/.openapi-generator/VERSION b/test/server/allany/AllAnyServer/.openapi-generator/VERSION deleted file mode 100644 index 96cfbb1..0000000 --- a/test/server/allany/AllAnyServer/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.13.0-SNAPSHOT diff --git a/test/server/allany/AllAnyServer/README.md b/test/server/allany/AllAnyServer/README.md deleted file mode 100644 index 0111a58..0000000 --- a/test/server/allany/AllAnyServer/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# Julia API server for AllAnyServer - -API to test code generation for oneof anyof allof - -## Overview -This API server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. - -- API version: 0.0.1 -- Generator version: 7.13.0-SNAPSHOT -- Build package: org.openapitools.codegen.languages.JuliaServerCodegen - - -## Installation -Place the Julia files generated under the `src` folder in your Julia project. Include AllAnyServer.jl in the project code. -It would include the module named AllAnyServer. - -Implement the server methods as listed below. They are also documented with the AllAnyServer module. -Launch a HTTP server with a router that has all handlers registered. A `register` method is provided in AllAnyServer module for convenience. - -```julia -register( - router::HTTP.Router, # Router to register handlers in - impl; # Module that implements the server methods - path_prefix::String="", # Prefix to be applied to all paths - optional_middlewares... # Optional middlewares to be applied to all handlers -) -``` - -Optional middlewares can be one or more of: -- `init`: called before the request is processed -- `pre_validation`: called after the request is parsed but before validation -- `pre_invoke`: called after validation but before the handler is invoked -- `post_invoke`: called after the handler is invoked but before the response is sent - -The order in which middlewares are invoked are: -`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` - - -## API Endpoints - -The following server methods must be implemented: - -Class | Method | HTTP request | Description ------------- | ------------- | ------------- | ------------- -*DefaultApi* | [**echo_anyof_base_type_post**](docs/DefaultApi.md#echo_anyof_base_type_post) | **POST** /echo_anyof_base_type | -*DefaultApi* | [**echo_anyof_mapped_pets_post**](docs/DefaultApi.md#echo_anyof_mapped_pets_post) | **POST** /echo_anyof_mapped_pets | -*DefaultApi* | [**echo_anyof_pets_post**](docs/DefaultApi.md#echo_anyof_pets_post) | **POST** /echo_anyof_pets | -*DefaultApi* | [**echo_arrays_post**](docs/DefaultApi.md#echo_arrays_post) | **POST** /echo_arrays | -*DefaultApi* | [**echo_oneof_base_type_post**](docs/DefaultApi.md#echo_oneof_base_type_post) | **POST** /echo_oneof_base_type | -*DefaultApi* | [**echo_oneof_mapped_pets_post**](docs/DefaultApi.md#echo_oneof_mapped_pets_post) | **POST** /echo_oneof_mapped_pets | -*DefaultApi* | [**echo_oneof_pets_post**](docs/DefaultApi.md#echo_oneof_pets_post) | **POST** /echo_oneof_pets | - - - -## Models - - - [AnyOfBaseType](docs/AnyOfBaseType.md) - - [AnyOfMappedPets](docs/AnyOfMappedPets.md) - - [AnyOfPets](docs/AnyOfPets.md) - - [Cat](docs/Cat.md) - - [Dog](docs/Dog.md) - - [OneOfBaseType](docs/OneOfBaseType.md) - - [OneOfMappedPets](docs/OneOfMappedPets.md) - - [OneOfPets](docs/OneOfPets.md) - - [Pet](docs/Pet.md) - - [TypeWithAllArrayTypes](docs/TypeWithAllArrayTypes.md) - - - -## Author - -test@example.com - diff --git a/test/server/allany/AllAnyServer/docs/AnyOfBaseType.md b/test/server/allany/AllAnyServer/docs/AnyOfBaseType.md deleted file mode 100644 index f4e20a9..0000000 --- a/test/server/allany/AllAnyServer/docs/AnyOfBaseType.md +++ /dev/null @@ -1,16 +0,0 @@ -# AnyOfBaseType - - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**value** | This is a anyOf model. The value must be any of the following types: Float64, String | | [optional] - - - - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/allany/AllAnyServer/docs/AnyOfMappedPets.md b/test/server/allany/AllAnyServer/docs/AnyOfMappedPets.md deleted file mode 100644 index de0a75e..0000000 --- a/test/server/allany/AllAnyServer/docs/AnyOfMappedPets.md +++ /dev/null @@ -1,19 +0,0 @@ -# AnyOfMappedPets - - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**value** | This is a anyOf model. The value must be any of the following types: Cat, Dog | | [optional] - -The discriminator field is `pet_type` with the following mapping: - - `cat`: `Cat` - - `dog`: `Dog` - - - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/allany/AllAnyServer/docs/AnyOfPets.md b/test/server/allany/AllAnyServer/docs/AnyOfPets.md deleted file mode 100644 index 3718118..0000000 --- a/test/server/allany/AllAnyServer/docs/AnyOfPets.md +++ /dev/null @@ -1,16 +0,0 @@ -# AnyOfPets - - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**value** | This is a anyOf model. The value must be any of the following types: Cat, Dog | | [optional] - -The discriminator field is `pet_type` - - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/allany/AllAnyServer/docs/Cat.md b/test/server/allany/AllAnyServer/docs/Cat.md deleted file mode 100644 index 606bbcf..0000000 --- a/test/server/allany/AllAnyServer/docs/Cat.md +++ /dev/null @@ -1,14 +0,0 @@ -# Cat - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**pet_type** | **String** | | [default to nothing] -**hunts** | **Bool** | | [optional] [default to nothing] -**age** | **Int64** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/allany/AllAnyServer/docs/CatAllOf.md b/test/server/allany/AllAnyServer/docs/CatAllOf.md deleted file mode 100644 index 3f72abe..0000000 --- a/test/server/allany/AllAnyServer/docs/CatAllOf.md +++ /dev/null @@ -1,13 +0,0 @@ -# CatAllOf - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**hunts** | **Bool** | | [optional] [default to nothing] -**age** | **Int64** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/allany/AllAnyServer/docs/DefaultApi.md b/test/server/allany/AllAnyServer/docs/DefaultApi.md deleted file mode 100644 index a3d457a..0000000 --- a/test/server/allany/AllAnyServer/docs/DefaultApi.md +++ /dev/null @@ -1,204 +0,0 @@ -# DefaultApi - -All URIs are relative to *http://localhost* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**echo_anyof_base_type_post**](DefaultApi.md#echo_anyof_base_type_post) | **POST** /echo_anyof_base_type | -[**echo_anyof_mapped_pets_post**](DefaultApi.md#echo_anyof_mapped_pets_post) | **POST** /echo_anyof_mapped_pets | -[**echo_anyof_pets_post**](DefaultApi.md#echo_anyof_pets_post) | **POST** /echo_anyof_pets | -[**echo_arrays_post**](DefaultApi.md#echo_arrays_post) | **POST** /echo_arrays | -[**echo_oneof_base_type_post**](DefaultApi.md#echo_oneof_base_type_post) | **POST** /echo_oneof_base_type | -[**echo_oneof_mapped_pets_post**](DefaultApi.md#echo_oneof_mapped_pets_post) | **POST** /echo_oneof_mapped_pets | -[**echo_oneof_pets_post**](DefaultApi.md#echo_oneof_pets_post) | **POST** /echo_oneof_pets | - - -# **echo_anyof_base_type_post** -> echo_anyof_base_type_post(req::HTTP.Request, any_of_base_type::AnyOfBaseType;) -> AnyOfBaseType - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**any_of_base_type** | [**AnyOfBaseType**](AnyOfBaseType.md)| | - -### Return type - -[**AnyOfBaseType**](AnyOfBaseType.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **echo_anyof_mapped_pets_post** -> echo_anyof_mapped_pets_post(req::HTTP.Request, any_of_mapped_pets::AnyOfMappedPets;) -> AnyOfMappedPets - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**any_of_mapped_pets** | [**AnyOfMappedPets**](AnyOfMappedPets.md)| | - -### Return type - -[**AnyOfMappedPets**](AnyOfMappedPets.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **echo_anyof_pets_post** -> echo_anyof_pets_post(req::HTTP.Request, any_of_pets::AnyOfPets;) -> AnyOfPets - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**any_of_pets** | [**AnyOfPets**](AnyOfPets.md)| | - -### Return type - -[**AnyOfPets**](AnyOfPets.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **echo_arrays_post** -> echo_arrays_post(req::HTTP.Request, type_with_all_array_types::TypeWithAllArrayTypes;) -> TypeWithAllArrayTypes - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**type_with_all_array_types** | [**TypeWithAllArrayTypes**](TypeWithAllArrayTypes.md)| | - -### Return type - -[**TypeWithAllArrayTypes**](TypeWithAllArrayTypes.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **echo_oneof_base_type_post** -> echo_oneof_base_type_post(req::HTTP.Request, one_of_base_type::OneOfBaseType;) -> OneOfBaseType - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**one_of_base_type** | [**OneOfBaseType**](OneOfBaseType.md)| | - -### Return type - -[**OneOfBaseType**](OneOfBaseType.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **echo_oneof_mapped_pets_post** -> echo_oneof_mapped_pets_post(req::HTTP.Request, one_of_mapped_pets::OneOfMappedPets;) -> OneOfMappedPets - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**one_of_mapped_pets** | [**OneOfMappedPets**](OneOfMappedPets.md)| | - -### Return type - -[**OneOfMappedPets**](OneOfMappedPets.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **echo_oneof_pets_post** -> echo_oneof_pets_post(req::HTTP.Request, one_of_pets::OneOfPets;) -> OneOfPets - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**one_of_pets** | [**OneOfPets**](OneOfPets.md)| | - -### Return type - -[**OneOfPets**](OneOfPets.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/test/server/allany/AllAnyServer/docs/Dog.md b/test/server/allany/AllAnyServer/docs/Dog.md deleted file mode 100644 index 6f348dc..0000000 --- a/test/server/allany/AllAnyServer/docs/Dog.md +++ /dev/null @@ -1,14 +0,0 @@ -# Dog - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**pet_type** | **String** | | [default to nothing] -**bark** | **Bool** | | [optional] [default to nothing] -**breed** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/allany/AllAnyServer/docs/DogAllOf.md b/test/server/allany/AllAnyServer/docs/DogAllOf.md deleted file mode 100644 index 28333b9..0000000 --- a/test/server/allany/AllAnyServer/docs/DogAllOf.md +++ /dev/null @@ -1,13 +0,0 @@ -# DogAllOf - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**bark** | **Bool** | | [optional] [default to nothing] -**breed** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/allany/AllAnyServer/docs/OneOfBaseType.md b/test/server/allany/AllAnyServer/docs/OneOfBaseType.md deleted file mode 100644 index 2347884..0000000 --- a/test/server/allany/AllAnyServer/docs/OneOfBaseType.md +++ /dev/null @@ -1,15 +0,0 @@ -# OneOfBaseType - - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**value** | This is a oneOf model. The value must be exactly one of the following types: Float64, String | | [optional] - - - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/allany/AllAnyServer/docs/OneOfMappedPets.md b/test/server/allany/AllAnyServer/docs/OneOfMappedPets.md deleted file mode 100644 index 8b329e0..0000000 --- a/test/server/allany/AllAnyServer/docs/OneOfMappedPets.md +++ /dev/null @@ -1,18 +0,0 @@ -# OneOfMappedPets - - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**value** | This is a oneOf model. The value must be exactly one of the following types: Cat, Dog | | [optional] - -The discriminator field is `pet_type` with the following mapping: - - `cat`: `Cat` - - `dog`: `Dog` - - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/allany/AllAnyServer/docs/OneOfPets.md b/test/server/allany/AllAnyServer/docs/OneOfPets.md deleted file mode 100644 index 3b2a95f..0000000 --- a/test/server/allany/AllAnyServer/docs/OneOfPets.md +++ /dev/null @@ -1,15 +0,0 @@ -# OneOfPets - - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**value** | This is a oneOf model. The value must be exactly one of the following types: Cat, Dog | | [optional] - -The discriminator field is `pet_type` - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/allany/AllAnyServer/docs/Pet.md b/test/server/allany/AllAnyServer/docs/Pet.md deleted file mode 100644 index 2b7fbbb..0000000 --- a/test/server/allany/AllAnyServer/docs/Pet.md +++ /dev/null @@ -1,12 +0,0 @@ -# Pet - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**pet_type** | **String** | | [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/allany/AllAnyServer/docs/TypeWithAllArrayTypes.md b/test/server/allany/AllAnyServer/docs/TypeWithAllArrayTypes.md deleted file mode 100644 index e3f3acf..0000000 --- a/test/server/allany/AllAnyServer/docs/TypeWithAllArrayTypes.md +++ /dev/null @@ -1,15 +0,0 @@ -# TypeWithAllArrayTypes - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**oneofbase** | [**Vector{OneOfBaseType}**](OneOfBaseType.md) | | [optional] [default to nothing] -**anyofbase** | [**Vector{AnyOfBaseType}**](AnyOfBaseType.md) | | [optional] [default to nothing] -**oneofpets** | [**Vector{OneOfPets}**](OneOfPets.md) | | [optional] [default to nothing] -**anyofpets** | [**Vector{AnyOfPets}**](AnyOfPets.md) | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/allany/AllAnyServer/src/AllAnyServer.jl b/test/server/allany/AllAnyServer/src/AllAnyServer.jl deleted file mode 100644 index d6f6c9a..0000000 --- a/test/server/allany/AllAnyServer/src/AllAnyServer.jl +++ /dev/null @@ -1,72 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw""" -Encapsulates generated server code for AllAnyServer - -The following server methods must be implemented: - -- **echo_anyof_base_type_post** - - *invocation:* POST /echo_anyof_base_type - - *signature:* echo_anyof_base_type_post(req::HTTP.Request, any_of_base_type::AnyOfBaseType;) -> AnyOfBaseType -- **echo_anyof_mapped_pets_post** - - *invocation:* POST /echo_anyof_mapped_pets - - *signature:* echo_anyof_mapped_pets_post(req::HTTP.Request, any_of_mapped_pets::AnyOfMappedPets;) -> AnyOfMappedPets -- **echo_anyof_pets_post** - - *invocation:* POST /echo_anyof_pets - - *signature:* echo_anyof_pets_post(req::HTTP.Request, any_of_pets::AnyOfPets;) -> AnyOfPets -- **echo_arrays_post** - - *invocation:* POST /echo_arrays - - *signature:* echo_arrays_post(req::HTTP.Request, type_with_all_array_types::TypeWithAllArrayTypes;) -> TypeWithAllArrayTypes -- **echo_oneof_base_type_post** - - *invocation:* POST /echo_oneof_base_type - - *signature:* echo_oneof_base_type_post(req::HTTP.Request, one_of_base_type::OneOfBaseType;) -> OneOfBaseType -- **echo_oneof_mapped_pets_post** - - *invocation:* POST /echo_oneof_mapped_pets - - *signature:* echo_oneof_mapped_pets_post(req::HTTP.Request, one_of_mapped_pets::OneOfMappedPets;) -> OneOfMappedPets -- **echo_oneof_pets_post** - - *invocation:* POST /echo_oneof_pets - - *signature:* echo_oneof_pets_post(req::HTTP.Request, one_of_pets::OneOfPets;) -> OneOfPets -""" -module AllAnyServer - -using HTTP -using URIs -using Dates -using TimeZones -using OpenAPI -using OpenAPI.Servers - -const API_VERSION = "0.0.1" - -include("modelincludes.jl") - -include("apis/api_DefaultApi.jl") - -""" -Register handlers for all APIs in this module in the supplied `Router` instance. - -Paramerets: -- `router`: Router to register handlers in -- `impl`: module that implements the server methods - -Optional parameters: -- `path_prefix`: prefix to be applied to all paths -- `optional_middlewares`: Register one or more optional middlewares to be applied to all requests. - -Optional middlewares can be one or more of: - - `init`: called before the request is processed - - `pre_validation`: called after the request is parsed but before validation - - `pre_invoke`: called after validation but before the handler is invoked - - `post_invoke`: called after the handler is invoked but before the response is sent - -The order in which middlewares are invoked are: -`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` -""" -function register(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - registerDefaultApi(router, impl; path_prefix=path_prefix, optional_middlewares...) - return router -end - -end # module AllAnyServer diff --git a/test/server/allany/AllAnyServer/src/apis/api_DefaultApi.jl b/test/server/allany/AllAnyServer/src/apis/api_DefaultApi.jl deleted file mode 100644 index d4fba67..0000000 --- a/test/server/allany/AllAnyServer/src/apis/api_DefaultApi.jl +++ /dev/null @@ -1,295 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -function echo_anyof_base_type_post_read(handler) - function echo_anyof_base_type_post_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["AnyOfBaseType"] = OpenAPI.Servers.to_param_type(AnyOfBaseType, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function echo_anyof_base_type_post_validate(handler) - function echo_anyof_base_type_post_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "echo_anyof_base_type_post" - - n = "AnyOfBaseType" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function echo_anyof_base_type_post_invoke(impl; post_invoke=nothing) - function echo_anyof_base_type_post_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.echo_anyof_base_type_post(req::HTTP.Request, openapi_params["AnyOfBaseType"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function echo_anyof_mapped_pets_post_read(handler) - function echo_anyof_mapped_pets_post_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["AnyOfMappedPets"] = OpenAPI.Servers.to_param_type(AnyOfMappedPets, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function echo_anyof_mapped_pets_post_validate(handler) - function echo_anyof_mapped_pets_post_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "echo_anyof_mapped_pets_post" - - n = "AnyOfMappedPets" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function echo_anyof_mapped_pets_post_invoke(impl; post_invoke=nothing) - function echo_anyof_mapped_pets_post_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.echo_anyof_mapped_pets_post(req::HTTP.Request, openapi_params["AnyOfMappedPets"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function echo_anyof_pets_post_read(handler) - function echo_anyof_pets_post_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["AnyOfPets"] = OpenAPI.Servers.to_param_type(AnyOfPets, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function echo_anyof_pets_post_validate(handler) - function echo_anyof_pets_post_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "echo_anyof_pets_post" - - n = "AnyOfPets" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function echo_anyof_pets_post_invoke(impl; post_invoke=nothing) - function echo_anyof_pets_post_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.echo_anyof_pets_post(req::HTTP.Request, openapi_params["AnyOfPets"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function echo_arrays_post_read(handler) - function echo_arrays_post_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["TypeWithAllArrayTypes"] = OpenAPI.Servers.to_param_type(TypeWithAllArrayTypes, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function echo_arrays_post_validate(handler) - function echo_arrays_post_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "echo_arrays_post" - - n = "TypeWithAllArrayTypes" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function echo_arrays_post_invoke(impl; post_invoke=nothing) - function echo_arrays_post_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.echo_arrays_post(req::HTTP.Request, openapi_params["TypeWithAllArrayTypes"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function echo_oneof_base_type_post_read(handler) - function echo_oneof_base_type_post_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["OneOfBaseType"] = OpenAPI.Servers.to_param_type(OneOfBaseType, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function echo_oneof_base_type_post_validate(handler) - function echo_oneof_base_type_post_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "echo_oneof_base_type_post" - - n = "OneOfBaseType" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function echo_oneof_base_type_post_invoke(impl; post_invoke=nothing) - function echo_oneof_base_type_post_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.echo_oneof_base_type_post(req::HTTP.Request, openapi_params["OneOfBaseType"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function echo_oneof_mapped_pets_post_read(handler) - function echo_oneof_mapped_pets_post_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["OneOfMappedPets"] = OpenAPI.Servers.to_param_type(OneOfMappedPets, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function echo_oneof_mapped_pets_post_validate(handler) - function echo_oneof_mapped_pets_post_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "echo_oneof_mapped_pets_post" - - n = "OneOfMappedPets" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function echo_oneof_mapped_pets_post_invoke(impl; post_invoke=nothing) - function echo_oneof_mapped_pets_post_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.echo_oneof_mapped_pets_post(req::HTTP.Request, openapi_params["OneOfMappedPets"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function echo_oneof_pets_post_read(handler) - function echo_oneof_pets_post_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["OneOfPets"] = OpenAPI.Servers.to_param_type(OneOfPets, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function echo_oneof_pets_post_validate(handler) - function echo_oneof_pets_post_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "echo_oneof_pets_post" - - n = "OneOfPets" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function echo_oneof_pets_post_invoke(impl; post_invoke=nothing) - function echo_oneof_pets_post_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.echo_oneof_pets_post(req::HTTP.Request, openapi_params["OneOfPets"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - - -function registerDefaultApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - HTTP.register!(router, "POST", path_prefix * "/echo_anyof_base_type", OpenAPI.Servers.middleware(impl, echo_anyof_base_type_post_read, echo_anyof_base_type_post_validate, echo_anyof_base_type_post_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/echo_anyof_mapped_pets", OpenAPI.Servers.middleware(impl, echo_anyof_mapped_pets_post_read, echo_anyof_mapped_pets_post_validate, echo_anyof_mapped_pets_post_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/echo_anyof_pets", OpenAPI.Servers.middleware(impl, echo_anyof_pets_post_read, echo_anyof_pets_post_validate, echo_anyof_pets_post_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/echo_arrays", OpenAPI.Servers.middleware(impl, echo_arrays_post_read, echo_arrays_post_validate, echo_arrays_post_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/echo_oneof_base_type", OpenAPI.Servers.middleware(impl, echo_oneof_base_type_post_read, echo_oneof_base_type_post_validate, echo_oneof_base_type_post_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/echo_oneof_mapped_pets", OpenAPI.Servers.middleware(impl, echo_oneof_mapped_pets_post_read, echo_oneof_mapped_pets_post_validate, echo_oneof_mapped_pets_post_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/echo_oneof_pets", OpenAPI.Servers.middleware(impl, echo_oneof_pets_post_read, echo_oneof_pets_post_validate, echo_oneof_pets_post_invoke; optional_middlewares...)) - return router -end diff --git a/test/server/allany/AllAnyServer/src/modelincludes.jl b/test/server/allany/AllAnyServer/src/modelincludes.jl deleted file mode 100644 index fe46dd5..0000000 --- a/test/server/allany/AllAnyServer/src/modelincludes.jl +++ /dev/null @@ -1,13 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -include("models/model_AnyOfBaseType.jl") -include("models/model_AnyOfMappedPets.jl") -include("models/model_AnyOfPets.jl") -include("models/model_Cat.jl") -include("models/model_Dog.jl") -include("models/model_OneOfBaseType.jl") -include("models/model_OneOfMappedPets.jl") -include("models/model_OneOfPets.jl") -include("models/model_Pet.jl") -include("models/model_TypeWithAllArrayTypes.jl") diff --git a/test/server/allany/AllAnyServer/src/models/model_AnyOfBaseType.jl b/test/server/allany/AllAnyServer/src/models/model_AnyOfBaseType.jl deleted file mode 100644 index 57a265c..0000000 --- a/test/server/allany/AllAnyServer/src/models/model_AnyOfBaseType.jl +++ /dev/null @@ -1,20 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - - -@doc raw"""AnyOfBaseType - - AnyOfBaseType(; value=nothing) -""" -mutable struct AnyOfBaseType <: OpenAPI.AnyOfAPIModel - value::Any # Union{ Float64, String } - AnyOfBaseType() = new() - AnyOfBaseType(value) = new(value) -end # type AnyOfBaseType - -function OpenAPI.property_type(::Type{ AnyOfBaseType }, name::Symbol, json::Dict{String,Any}) - - # no discriminator specified, can't determine the exact type - return fieldtype(AnyOfBaseType, name) -end diff --git a/test/server/allany/AllAnyServer/src/models/model_AnyOfMappedPets.jl b/test/server/allany/AllAnyServer/src/models/model_AnyOfMappedPets.jl deleted file mode 100644 index 9522b4f..0000000 --- a/test/server/allany/AllAnyServer/src/models/model_AnyOfMappedPets.jl +++ /dev/null @@ -1,24 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - - -@doc raw"""AnyOfMappedPets - - AnyOfMappedPets(; value=nothing) -""" -mutable struct AnyOfMappedPets <: OpenAPI.AnyOfAPIModel - value::Any # Union{ Cat, Dog } - AnyOfMappedPets() = new() - AnyOfMappedPets(value) = new(value) -end # type AnyOfMappedPets - -function OpenAPI.property_type(::Type{ AnyOfMappedPets }, name::Symbol, json::Dict{String,Any}) - discriminator = json["pet_type"] - if discriminator == "cat" - return eval(Base.Meta.parse("Cat")) - elseif discriminator == "dog" - return eval(Base.Meta.parse("Dog")) - end - throw(OpenAPI.ValidationException("Invalid discriminator value: $discriminator for AnyOfMappedPets")) -end diff --git a/test/server/allany/AllAnyServer/src/models/model_AnyOfPets.jl b/test/server/allany/AllAnyServer/src/models/model_AnyOfPets.jl deleted file mode 100644 index fa158d7..0000000 --- a/test/server/allany/AllAnyServer/src/models/model_AnyOfPets.jl +++ /dev/null @@ -1,24 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - - -@doc raw"""AnyOfPets - - AnyOfPets(; value=nothing) -""" -mutable struct AnyOfPets <: OpenAPI.AnyOfAPIModel - value::Any # Union{ Cat, Dog } - AnyOfPets() = new() - AnyOfPets(value) = new(value) -end # type AnyOfPets - -function OpenAPI.property_type(::Type{ AnyOfPets }, name::Symbol, json::Dict{String,Any}) - discriminator = json["pet_type"] - if discriminator == "Cat" - return eval(Base.Meta.parse("Cat")) - elseif discriminator == "Dog" - return eval(Base.Meta.parse("Dog")) - end - throw(OpenAPI.ValidationException("Invalid discriminator value: $discriminator for AnyOfPets")) -end diff --git a/test/server/allany/AllAnyServer/src/models/model_Cat.jl b/test/server/allany/AllAnyServer/src/models/model_Cat.jl deleted file mode 100644 index 1f720cf..0000000 --- a/test/server/allany/AllAnyServer/src/models/model_Cat.jl +++ /dev/null @@ -1,47 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Cat - - Cat(; - pet_type=nothing, - hunts=nothing, - age=nothing, - ) - - - pet_type::String - - hunts::Bool - - age::Int64 -""" -Base.@kwdef mutable struct Cat <: OpenAPI.APIModel - pet_type::Union{Nothing, String} = nothing - hunts::Union{Nothing, Bool} = nothing - age::Union{Nothing, Int64} = nothing - - function Cat(pet_type, hunts, age, ) - o = new(pet_type, hunts, age, ) - OpenAPI.validate_properties(o) - return o - end -end # type Cat - -const _property_types_Cat = Dict{Symbol,String}(Symbol("pet_type")=>"String", Symbol("hunts")=>"Bool", Symbol("age")=>"Int64", ) -OpenAPI.property_type(::Type{ Cat }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Cat[name]))} - -function OpenAPI.check_required(o::Cat) - o.pet_type === nothing && (return false) - true -end - -function OpenAPI.validate_properties(o::Cat) - OpenAPI.validate_property(Cat, Symbol("pet_type"), o.pet_type) - OpenAPI.validate_property(Cat, Symbol("hunts"), o.hunts) - OpenAPI.validate_property(Cat, Symbol("age"), o.age) -end - -function OpenAPI.validate_property(::Type{ Cat }, name::Symbol, val) - - - -end diff --git a/test/server/allany/AllAnyServer/src/models/model_CatAllOf.jl b/test/server/allany/AllAnyServer/src/models/model_CatAllOf.jl deleted file mode 100644 index e9f19a0..0000000 --- a/test/server/allany/AllAnyServer/src/models/model_CatAllOf.jl +++ /dev/null @@ -1,33 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw""" - CatAllOf(; - hunts=nothing, - age=nothing, - ) - - - hunts::Bool - - age::Int64 -""" -Base.@kwdef mutable struct CatAllOf <: OpenAPI.APIModel - hunts::Union{Nothing, Bool} = nothing - age::Union{Nothing, Int64} = nothing - - function CatAllOf(hunts, age, ) - OpenAPI.validate_property(CatAllOf, Symbol("hunts"), hunts) - OpenAPI.validate_property(CatAllOf, Symbol("age"), age) - return new(hunts, age, ) - end -end # type CatAllOf - -const _property_types_CatAllOf = Dict{Symbol,String}(Symbol("hunts")=>"Bool", Symbol("age")=>"Int64", ) -OpenAPI.property_type(::Type{ CatAllOf }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_CatAllOf[name]))} - -function check_required(o::CatAllOf) - true -end - -function OpenAPI.validate_property(::Type{ CatAllOf }, name::Symbol, val) -end diff --git a/test/server/allany/AllAnyServer/src/models/model_Dog.jl b/test/server/allany/AllAnyServer/src/models/model_Dog.jl deleted file mode 100644 index c5ac6b9..0000000 --- a/test/server/allany/AllAnyServer/src/models/model_Dog.jl +++ /dev/null @@ -1,51 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Dog - - Dog(; - pet_type=nothing, - bark=nothing, - breed=nothing, - ) - - - pet_type::String - - bark::Bool - - breed::String -""" -Base.@kwdef mutable struct Dog <: OpenAPI.APIModel - pet_type::Union{Nothing, String} = nothing - bark::Union{Nothing, Bool} = nothing - breed::Union{Nothing, String} = nothing - - function Dog(pet_type, bark, breed, ) - o = new(pet_type, bark, breed, ) - OpenAPI.validate_properties(o) - return o - end -end # type Dog - -const _property_types_Dog = Dict{Symbol,String}(Symbol("pet_type")=>"String", Symbol("bark")=>"Bool", Symbol("breed")=>"String", ) -OpenAPI.property_type(::Type{ Dog }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Dog[name]))} - -function OpenAPI.check_required(o::Dog) - o.pet_type === nothing && (return false) - true -end - -function OpenAPI.validate_properties(o::Dog) - OpenAPI.validate_property(Dog, Symbol("pet_type"), o.pet_type) - OpenAPI.validate_property(Dog, Symbol("bark"), o.bark) - OpenAPI.validate_property(Dog, Symbol("breed"), o.breed) -end - -function OpenAPI.validate_property(::Type{ Dog }, name::Symbol, val) - - - - if name === Symbol("breed") - OpenAPI.validate_param(name, "Dog", :enum, val, ["Dingo", "Husky", "Retriever", "Shepherd"]) - end - -end diff --git a/test/server/allany/AllAnyServer/src/models/model_DogAllOf.jl b/test/server/allany/AllAnyServer/src/models/model_DogAllOf.jl deleted file mode 100644 index 62869e7..0000000 --- a/test/server/allany/AllAnyServer/src/models/model_DogAllOf.jl +++ /dev/null @@ -1,36 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw""" - DogAllOf(; - bark=nothing, - breed=nothing, - ) - - - bark::Bool - - breed::String -""" -Base.@kwdef mutable struct DogAllOf <: OpenAPI.APIModel - bark::Union{Nothing, Bool} = nothing - breed::Union{Nothing, String} = nothing - - function DogAllOf(bark, breed, ) - OpenAPI.validate_property(DogAllOf, Symbol("bark"), bark) - OpenAPI.validate_property(DogAllOf, Symbol("breed"), breed) - return new(bark, breed, ) - end -end # type DogAllOf - -const _property_types_DogAllOf = Dict{Symbol,String}(Symbol("bark")=>"Bool", Symbol("breed")=>"String", ) -OpenAPI.property_type(::Type{ DogAllOf }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_DogAllOf[name]))} - -function check_required(o::DogAllOf) - true -end - -function OpenAPI.validate_property(::Type{ DogAllOf }, name::Symbol, val) - if name === Symbol("breed") - OpenAPI.validate_param(name, "DogAllOf", :enum, val, ["Dingo", "Husky", "Retriever", "Shepherd"]) - end -end diff --git a/test/server/allany/AllAnyServer/src/models/model_OneOfBaseType.jl b/test/server/allany/AllAnyServer/src/models/model_OneOfBaseType.jl deleted file mode 100644 index 073188f..0000000 --- a/test/server/allany/AllAnyServer/src/models/model_OneOfBaseType.jl +++ /dev/null @@ -1,20 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - - -@doc raw"""OneOfBaseType - - OneOfBaseType(; value=nothing) -""" -mutable struct OneOfBaseType <: OpenAPI.OneOfAPIModel - value::Any # Union{ Float64, String } - OneOfBaseType() = new() - OneOfBaseType(value) = new(value) -end # type OneOfBaseType - -function OpenAPI.property_type(::Type{ OneOfBaseType }, name::Symbol, json::Dict{String,Any}) - - # no discriminator specified, can't determine the exact type - return fieldtype(OneOfBaseType, name) -end diff --git a/test/server/allany/AllAnyServer/src/models/model_OneOfMappedPets.jl b/test/server/allany/AllAnyServer/src/models/model_OneOfMappedPets.jl deleted file mode 100644 index afe696f..0000000 --- a/test/server/allany/AllAnyServer/src/models/model_OneOfMappedPets.jl +++ /dev/null @@ -1,24 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - - -@doc raw"""OneOfMappedPets - - OneOfMappedPets(; value=nothing) -""" -mutable struct OneOfMappedPets <: OpenAPI.OneOfAPIModel - value::Any # Union{ Cat, Dog } - OneOfMappedPets() = new() - OneOfMappedPets(value) = new(value) -end # type OneOfMappedPets - -function OpenAPI.property_type(::Type{ OneOfMappedPets }, name::Symbol, json::Dict{String,Any}) - discriminator = json["pet_type"] - if discriminator == "cat" - return eval(Base.Meta.parse("Cat")) - elseif discriminator == "dog" - return eval(Base.Meta.parse("Dog")) - end - throw(OpenAPI.ValidationException("Invalid discriminator value: $discriminator for OneOfMappedPets")) -end diff --git a/test/server/allany/AllAnyServer/src/models/model_OneOfPets.jl b/test/server/allany/AllAnyServer/src/models/model_OneOfPets.jl deleted file mode 100644 index a3fe448..0000000 --- a/test/server/allany/AllAnyServer/src/models/model_OneOfPets.jl +++ /dev/null @@ -1,24 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - - -@doc raw"""OneOfPets - - OneOfPets(; value=nothing) -""" -mutable struct OneOfPets <: OpenAPI.OneOfAPIModel - value::Any # Union{ Cat, Dog } - OneOfPets() = new() - OneOfPets(value) = new(value) -end # type OneOfPets - -function OpenAPI.property_type(::Type{ OneOfPets }, name::Symbol, json::Dict{String,Any}) - discriminator = json["pet_type"] - if discriminator == "Cat" - return eval(Base.Meta.parse("Cat")) - elseif discriminator == "Dog" - return eval(Base.Meta.parse("Dog")) - end - throw(OpenAPI.ValidationException("Invalid discriminator value: $discriminator for OneOfPets")) -end diff --git a/test/server/allany/AllAnyServer/src/models/model_Pet.jl b/test/server/allany/AllAnyServer/src/models/model_Pet.jl deleted file mode 100644 index e95c0d4..0000000 --- a/test/server/allany/AllAnyServer/src/models/model_Pet.jl +++ /dev/null @@ -1,37 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Pet - - Pet(; - pet_type=nothing, - ) - - - pet_type::String -""" -Base.@kwdef mutable struct Pet <: OpenAPI.APIModel - pet_type::Union{Nothing, String} = nothing - - function Pet(pet_type, ) - o = new(pet_type, ) - OpenAPI.validate_properties(o) - return o - end -end # type Pet - -const _property_types_Pet = Dict{Symbol,String}(Symbol("pet_type")=>"String", ) -OpenAPI.property_type(::Type{ Pet }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Pet[name]))} - -function OpenAPI.check_required(o::Pet) - o.pet_type === nothing && (return false) - true -end - -function OpenAPI.validate_properties(o::Pet) - OpenAPI.validate_property(Pet, Symbol("pet_type"), o.pet_type) -end - -function OpenAPI.validate_property(::Type{ Pet }, name::Symbol, val) - -end diff --git a/test/server/allany/AllAnyServer/src/models/model_TypeWithAllArrayTypes.jl b/test/server/allany/AllAnyServer/src/models/model_TypeWithAllArrayTypes.jl deleted file mode 100644 index 5236187..0000000 --- a/test/server/allany/AllAnyServer/src/models/model_TypeWithAllArrayTypes.jl +++ /dev/null @@ -1,51 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""TypeWithAllArrayTypes - - TypeWithAllArrayTypes(; - oneofbase=nothing, - anyofbase=nothing, - oneofpets=nothing, - anyofpets=nothing, - ) - - - oneofbase::Vector{OneOfBaseType} - - anyofbase::Vector{AnyOfBaseType} - - oneofpets::Vector{OneOfPets} - - anyofpets::Vector{AnyOfPets} -""" -Base.@kwdef mutable struct TypeWithAllArrayTypes <: OpenAPI.APIModel - oneofbase::Union{Nothing, Vector} = nothing # spec type: Union{ Nothing, Vector{OneOfBaseType} } - anyofbase::Union{Nothing, Vector} = nothing # spec type: Union{ Nothing, Vector{AnyOfBaseType} } - oneofpets::Union{Nothing, Vector} = nothing # spec type: Union{ Nothing, Vector{OneOfPets} } - anyofpets::Union{Nothing, Vector} = nothing # spec type: Union{ Nothing, Vector{AnyOfPets} } - - function TypeWithAllArrayTypes(oneofbase, anyofbase, oneofpets, anyofpets, ) - o = new(oneofbase, anyofbase, oneofpets, anyofpets, ) - OpenAPI.validate_properties(o) - return o - end -end # type TypeWithAllArrayTypes - -const _property_types_TypeWithAllArrayTypes = Dict{Symbol,String}(Symbol("oneofbase")=>"Vector{OneOfBaseType}", Symbol("anyofbase")=>"Vector{AnyOfBaseType}", Symbol("oneofpets")=>"Vector{OneOfPets}", Symbol("anyofpets")=>"Vector{AnyOfPets}", ) -OpenAPI.property_type(::Type{ TypeWithAllArrayTypes }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_TypeWithAllArrayTypes[name]))} - -function OpenAPI.check_required(o::TypeWithAllArrayTypes) - true -end - -function OpenAPI.validate_properties(o::TypeWithAllArrayTypes) - OpenAPI.validate_property(TypeWithAllArrayTypes, Symbol("oneofbase"), o.oneofbase) - OpenAPI.validate_property(TypeWithAllArrayTypes, Symbol("anyofbase"), o.anyofbase) - OpenAPI.validate_property(TypeWithAllArrayTypes, Symbol("oneofpets"), o.oneofpets) - OpenAPI.validate_property(TypeWithAllArrayTypes, Symbol("anyofpets"), o.anyofpets) -end - -function OpenAPI.validate_property(::Type{ TypeWithAllArrayTypes }, name::Symbol, val) - - - - -end diff --git a/test/server/allany/allany_server.jl b/test/server/allany/allany_server.jl deleted file mode 100644 index 09e4da4..0000000 --- a/test/server/allany/allany_server.jl +++ /dev/null @@ -1,98 +0,0 @@ -module AllAnyServerImpl - -using HTTP - -include("AllAnyServer/src/AllAnyServer.jl") - -using .AllAnyServer - -const server = Ref{Any}(nothing) - -""" -echo_arrays_post - -*invocation:* POST /echo_arrays -""" -function echo_arrays_post(req::HTTP.Request, type_with_all_array_types::AllAnyServer.TypeWithAllArrayTypes;) :: AllAnyServer.TypeWithAllArrayTypes - return type_with_all_array_types -end - -""" -echo_anyof_base_type_post - -*invocation:* POST /echo_anyof_base_type -""" -function echo_anyof_base_type_post(req::HTTP.Request, any_of_base_type::AllAnyServer.AnyOfBaseType;) :: AllAnyServer.AnyOfBaseType - return any_of_base_type -end - -""" -echo_oneof_base_type_post - -*invocation:* POST /echo_oneof_base_type -""" -function echo_oneof_base_type_post(req::HTTP.Request, one_of_base_type::AllAnyServer.OneOfBaseType;) :: AllAnyServer.OneOfBaseType - return one_of_base_type -end - -""" -echo_anyof_mapped_pets_post - -*invocation:* POST /echo_anyof_mapped_pets -""" -function echo_anyof_mapped_pets_post(req::HTTP.Request, any_of_mapped_pets::AllAnyServer.AnyOfMappedPets,) :: AllAnyServer.AnyOfMappedPets - return any_of_mapped_pets -end - -""" -echo_anyof_pets_post - -*invocation:* POST /echo_anyof_pets -""" -function echo_anyof_pets_post(req::HTTP.Request, any_of_pets::AllAnyServer.AnyOfPets,) :: AllAnyServer.AnyOfPets - return any_of_pets -end - -""" -echo_oneof_mapped_pets_post - -*invocation:* POST /echo_oneof_mapped_pets -""" -function echo_oneof_mapped_pets_post(req::HTTP.Request, one_of_mapped_pets::AllAnyServer.OneOfMappedPets,) :: AllAnyServer.OneOfMappedPets - return one_of_mapped_pets -end - -""" -echo_oneof_pets_post - -*invocation:* POST /echo_oneof_pets -""" -function echo_oneof_pets_post(req::HTTP.Request, one_of_pets::AllAnyServer.OneOfPets,) :: AllAnyServer.OneOfPets - return one_of_pets -end - -function stop(::HTTP.Request) - HTTP.close(server[]) - return HTTP.Response(200, "") -end - -function ping(::HTTP.Request) - return HTTP.Response(200, "") -end - -function run_server(port=8081) - try - router = HTTP.Router() - router = AllAnyServer.register(router, @__MODULE__) - HTTP.register!(router, "GET", "/stop", stop) - HTTP.register!(router, "GET", "/ping", ping) - server[] = HTTP.serve!(router, port) - wait(server[]) - catch ex - @error("Server error", exception=(ex, catch_backtrace())) - end -end - -end # module AllAnyServerImpl - -AllAnyServerImpl.run_server() \ No newline at end of file diff --git a/test/server/allany/generate.sh b/test/server/allany/generate.sh deleted file mode 100755 index 81499b5..0000000 --- a/test/server/allany/generate.sh +++ /dev/null @@ -1 +0,0 @@ -java -jar openapi-generator-cli.jar generate -i ../../specs/allany.yaml -g julia-server -o AllAnyServer --additional-properties=packageName=AllAnyServer diff --git a/test/server/juliahub/specs/juliahubrunner/src/apis/api_JobRunnerApi.jl b/test/server/juliahub/specs/juliahubrunner/src/apis/api_JobRunnerApi.jl deleted file mode 100644 index b28304d..0000000 --- a/test/server/juliahub/specs/juliahubrunner/src/apis/api_JobRunnerApi.jl +++ /dev/null @@ -1,298 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -function file_upload_finalize_read(handler) - function file_upload_finalize_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["job_id"] = OpenAPI.Servers.to_param(String, path_params, "job_id", required=true, ) - openapi_params["UploadFileDetails"] = OpenAPI.Servers.to_param_type(UploadFileDetails, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function file_upload_finalize_validate(handler) - function file_upload_finalize_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - OpenAPI.validate_param("job_id", "file_upload_finalize", :maxLength, openapi_params["job_id"], 13) - OpenAPI.validate_param("job_id", "file_upload_finalize", :minLength, openapi_params["job_id"], 10) - - return handler(req) - end -end - -function file_upload_finalize_invoke(impl; post_invoke=nothing) - function file_upload_finalize_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.file_upload_finalize(req::HTTP.Request, openapi_params["job_id"], openapi_params["UploadFileDetails"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function file_upload_init_read(handler) - function file_upload_init_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["job_id"] = OpenAPI.Servers.to_param(String, path_params, "job_id", required=true, ) - openapi_params["UploadFileDetails"] = OpenAPI.Servers.to_param_type(UploadFileDetails, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function file_upload_init_validate(handler) - function file_upload_init_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - OpenAPI.validate_param("job_id", "file_upload_init", :maxLength, openapi_params["job_id"], 13) - OpenAPI.validate_param("job_id", "file_upload_init", :minLength, openapi_params["job_id"], 10) - - return handler(req) - end -end - -function file_upload_init_invoke(impl; post_invoke=nothing) - function file_upload_init_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.file_upload_init(req::HTTP.Request, openapi_params["job_id"], openapi_params["UploadFileDetails"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_clusterinfo_read(handler) - function get_clusterinfo_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["job_id"] = OpenAPI.Servers.to_param(String, path_params, "job_id", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_clusterinfo_validate(handler) - function get_clusterinfo_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - OpenAPI.validate_param("job_id", "get_clusterinfo", :maxLength, openapi_params["job_id"], 13) - OpenAPI.validate_param("job_id", "get_clusterinfo", :minLength, openapi_params["job_id"], 10) - - return handler(req) - end -end - -function get_clusterinfo_invoke(impl; post_invoke=nothing) - function get_clusterinfo_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_clusterinfo(req::HTTP.Request, openapi_params["job_id"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_dataset_credentials_read(handler) - function get_dataset_credentials_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["dataset_id"] = OpenAPI.Servers.to_param(String, path_params, "dataset_id", required=true, ) - headers = Dict{String,String}(req.headers) - openapi_params["X-JuliaHub-JobId"] = OpenAPI.Servers.to_param(String, headers, "X-JuliaHub-JobId", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_dataset_credentials_validate(handler) - function get_dataset_credentials_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - OpenAPI.validate_param("dataset_id", "get_dataset_credentials", :maxLength, openapi_params["dataset_id"], 36) - OpenAPI.validate_param("dataset_id", "get_dataset_credentials", :minLength, openapi_params["dataset_id"], 36) - - OpenAPI.validate_param("X-JuliaHub-JobId", "get_dataset_credentials", :maxLength, openapi_params["X-JuliaHub-JobId"], 13) - OpenAPI.validate_param("X-JuliaHub-JobId", "get_dataset_credentials", :minLength, openapi_params["X-JuliaHub-JobId"], 10) - - return handler(req) - end -end - -function get_dataset_credentials_invoke(impl; post_invoke=nothing) - function get_dataset_credentials_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_dataset_credentials(req::HTTP.Request, openapi_params["dataset_id"], openapi_params["X-JuliaHub-JobId"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_datasets_read(handler) - function get_datasets_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_datasets_validate(handler) - function get_datasets_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - return handler(req) - end -end - -function get_datasets_invoke(impl; post_invoke=nothing) - function get_datasets_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_datasets(req::HTTP.Request;) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_job_input_read(handler) - function get_job_input_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["job_id"] = OpenAPI.Servers.to_param(String, path_params, "job_id", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_job_input_validate(handler) - function get_job_input_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - OpenAPI.validate_param("job_id", "get_job_input", :maxLength, openapi_params["job_id"], 13) - OpenAPI.validate_param("job_id", "get_job_input", :minLength, openapi_params["job_id"], 10) - - return handler(req) - end -end - -function get_job_input_invoke(impl; post_invoke=nothing) - function get_job_input_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_job_input(req::HTTP.Request, openapi_params["job_id"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_user_datasets_read(handler) - function get_user_datasets_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - query_params = HTTP.queryparams(URIs.URI(req.target)) - openapi_params["name"] = OpenAPI.Servers.to_param(String, query_params, "name", style="form", is_explode=true) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_user_datasets_validate(handler) - function get_user_datasets_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - - return handler(req) - end -end - -function get_user_datasets_invoke(impl; post_invoke=nothing) - function get_user_datasets_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_user_datasets(req::HTTP.Request; name=get(openapi_params, "name", nothing),) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function update_job_distributed_status_read(handler) - function update_job_distributed_status_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["job_id"] = OpenAPI.Servers.to_param(String, path_params, "job_id", required=true, ) - openapi_params["JobDistributedStatus"] = OpenAPI.Servers.to_param_type(JobDistributedStatus, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function update_job_distributed_status_validate(handler) - function update_job_distributed_status_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - OpenAPI.validate_param("job_id", "update_job_distributed_status", :maxLength, openapi_params["job_id"], 13) - OpenAPI.validate_param("job_id", "update_job_distributed_status", :minLength, openapi_params["job_id"], 10) - - return handler(req) - end -end - -function update_job_distributed_status_invoke(impl; post_invoke=nothing) - function update_job_distributed_status_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.update_job_distributed_status(req::HTTP.Request, openapi_params["job_id"], openapi_params["JobDistributedStatus"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function update_job_status_read(handler) - function update_job_status_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["job_id"] = OpenAPI.Servers.to_param(String, path_params, "job_id", required=true, ) - openapi_params["JobStatus"] = OpenAPI.Servers.to_param_type(JobStatus, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function update_job_status_validate(handler) - function update_job_status_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - OpenAPI.validate_param("job_id", "update_job_status", :maxLength, openapi_params["job_id"], 13) - OpenAPI.validate_param("job_id", "update_job_status", :minLength, openapi_params["job_id"], 10) - - return handler(req) - end -end - -function update_job_status_invoke(impl; post_invoke=nothing) - function update_job_status_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.update_job_status(req::HTTP.Request, openapi_params["job_id"], openapi_params["JobStatus"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - - -function registerJobRunnerApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - HTTP.register!(router, "POST", path_prefix * "/jobs/{job_id}/file_uploads", OpenAPI.Servers.middleware(impl, file_upload_finalize_read, file_upload_finalize_validate, file_upload_finalize_invoke; optional_middlewares...)) - HTTP.register!(router, "PUT", path_prefix * "/jobs/{job_id}/file_uploads", OpenAPI.Servers.middleware(impl, file_upload_init_read, file_upload_init_validate, file_upload_init_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/jobs/{job_id}/clusterinfo", OpenAPI.Servers.middleware(impl, get_clusterinfo_read, get_clusterinfo_validate, get_clusterinfo_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/datasets/{dataset_id}/credentials", OpenAPI.Servers.middleware(impl, get_dataset_credentials_read, get_dataset_credentials_validate, get_dataset_credentials_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/datasets", OpenAPI.Servers.middleware(impl, get_datasets_read, get_datasets_validate, get_datasets_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/jobs/{job_id}/input", OpenAPI.Servers.middleware(impl, get_job_input_read, get_job_input_validate, get_job_input_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/user/datasets", OpenAPI.Servers.middleware(impl, get_user_datasets_read, get_user_datasets_validate, get_user_datasets_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/jobs/{job_id}/distributed_status", OpenAPI.Servers.middleware(impl, update_job_distributed_status_read, update_job_distributed_status_validate, update_job_distributed_status_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/jobs/{job_id}/status", OpenAPI.Servers.middleware(impl, update_job_status_read, update_job_status_validate, update_job_status_invoke; optional_middlewares...)) - return router -end diff --git a/test/server/openapigenerator_petstore_v3/generate.sh b/test/server/openapigenerator_petstore_v3/generate.sh deleted file mode 100755 index e04d311..0000000 --- a/test/server/openapigenerator_petstore_v3/generate.sh +++ /dev/null @@ -1,6 +0,0 @@ -java -jar openapi-generator-cli.jar generate \ - -i ../../specs/openapigenerator_petstore_v3.json \ - -g julia-server \ - -o petstore \ - --additional-properties=packageName=OpenAPIGenPetStoreServer \ - --additional-properties=exportModels=true diff --git a/test/server/openapigenerator_petstore_v3/petstore/.openapi-generator-ignore b/test/server/openapigenerator_petstore_v3/petstore/.openapi-generator-ignore deleted file mode 100644 index 7484ee5..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/test/server/openapigenerator_petstore_v3/petstore/.openapi-generator/FILES b/test/server/openapigenerator_petstore_v3/petstore/.openapi-generator/FILES deleted file mode 100644 index 21bc78e..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/.openapi-generator/FILES +++ /dev/null @@ -1,21 +0,0 @@ -README.md -docs/ApiResponse.md -docs/Category.md -docs/Order.md -docs/Pet.md -docs/PetApi.md -docs/StoreApi.md -docs/Tag.md -docs/User.md -docs/UserApi.md -src/OpenAPIGenPetStoreServer.jl -src/apis/api_PetApi.jl -src/apis/api_StoreApi.jl -src/apis/api_UserApi.jl -src/modelincludes.jl -src/models/model_ApiResponse.jl -src/models/model_Category.jl -src/models/model_Order.jl -src/models/model_Pet.jl -src/models/model_Tag.jl -src/models/model_User.jl diff --git a/test/server/openapigenerator_petstore_v3/petstore/.openapi-generator/VERSION b/test/server/openapigenerator_petstore_v3/petstore/.openapi-generator/VERSION deleted file mode 100644 index 96cfbb1..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.13.0-SNAPSHOT diff --git a/test/server/openapigenerator_petstore_v3/petstore/README.md b/test/server/openapigenerator_petstore_v3/petstore/README.md deleted file mode 100644 index 87a8bcf..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# Julia API server for OpenAPIGenPetStoreServer - -This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. - -## Overview -This API server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. - -- API version: 1.0.0 -- Generator version: 7.13.0-SNAPSHOT -- Build package: org.openapitools.codegen.languages.JuliaServerCodegen - - -## Installation -Place the Julia files generated under the `src` folder in your Julia project. Include OpenAPIGenPetStoreServer.jl in the project code. -It would include the module named OpenAPIGenPetStoreServer. - -Implement the server methods as listed below. They are also documented with the OpenAPIGenPetStoreServer module. -Launch a HTTP server with a router that has all handlers registered. A `register` method is provided in OpenAPIGenPetStoreServer module for convenience. - -```julia -register( - router::HTTP.Router, # Router to register handlers in - impl; # Module that implements the server methods - path_prefix::String="", # Prefix to be applied to all paths - optional_middlewares... # Optional middlewares to be applied to all handlers -) -``` - -Optional middlewares can be one or more of: -- `init`: called before the request is processed -- `pre_validation`: called after the request is parsed but before validation -- `pre_invoke`: called after validation but before the handler is invoked -- `post_invoke`: called after the handler is invoked but before the response is sent - -The order in which middlewares are invoked are: -`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` - - -## API Endpoints - -The following server methods must be implemented: - -Class | Method | HTTP request | Description ------------- | ------------- | ------------- | ------------- -*PetApi* | [**add_pet**](docs/PetApi.md#add_pet) | **POST** /pet | Add a new pet to the store -*PetApi* | [**delete_pet**](docs/PetApi.md#delete_pet) | **DELETE** /pet/{petId} | Deletes a pet -*PetApi* | [**find_pets_by_status**](docs/PetApi.md#find_pets_by_status) | **GET** /pet/findByStatus | Finds Pets by status -*PetApi* | [**find_pets_by_tags**](docs/PetApi.md#find_pets_by_tags) | **GET** /pet/findByTags | Finds Pets by tags -*PetApi* | [**get_pet_by_id**](docs/PetApi.md#get_pet_by_id) | **GET** /pet/{petId} | Find pet by ID -*PetApi* | [**update_pet**](docs/PetApi.md#update_pet) | **PUT** /pet | Update an existing pet -*PetApi* | [**update_pet_with_form**](docs/PetApi.md#update_pet_with_form) | **POST** /pet/{petId} | Updates a pet in the store with form data -*PetApi* | [**upload_file**](docs/PetApi.md#upload_file) | **POST** /pet/{petId}/uploadImage | uploads an image -*StoreApi* | [**delete_order**](docs/StoreApi.md#delete_order) | **DELETE** /store/order/{orderId} | Delete purchase order by ID -*StoreApi* | [**get_inventory**](docs/StoreApi.md#get_inventory) | **GET** /store/inventory | Returns pet inventories by status -*StoreApi* | [**get_order_by_id**](docs/StoreApi.md#get_order_by_id) | **GET** /store/order/{orderId} | Find purchase order by ID -*StoreApi* | [**place_order**](docs/StoreApi.md#place_order) | **POST** /store/order | Place an order for a pet -*UserApi* | [**create_user**](docs/UserApi.md#create_user) | **POST** /user | Create user -*UserApi* | [**create_users_with_array_input**](docs/UserApi.md#create_users_with_array_input) | **POST** /user/createWithArray | Creates list of users with given input array -*UserApi* | [**create_users_with_list_input**](docs/UserApi.md#create_users_with_list_input) | **POST** /user/createWithList | Creates list of users with given input array -*UserApi* | [**delete_user**](docs/UserApi.md#delete_user) | **DELETE** /user/{username} | Delete user -*UserApi* | [**get_user_by_name**](docs/UserApi.md#get_user_by_name) | **GET** /user/{username} | Get user by user name -*UserApi* | [**login_user**](docs/UserApi.md#login_user) | **GET** /user/login | Logs user into the system -*UserApi* | [**logout_user**](docs/UserApi.md#logout_user) | **GET** /user/logout | Logs out current logged in user session -*UserApi* | [**update_user**](docs/UserApi.md#update_user) | **PUT** /user/{username} | Updated user - - - -## Models - - - [ApiResponse](docs/ApiResponse.md) - - [Category](docs/Category.md) - - [Order](docs/Order.md) - - [Pet](docs/Pet.md) - - [Tag](docs/Tag.md) - - [User](docs/User.md) - - - -## Author - - - diff --git a/test/server/openapigenerator_petstore_v3/petstore/docs/ApiResponse.md b/test/server/openapigenerator_petstore_v3/petstore/docs/ApiResponse.md deleted file mode 100644 index a7a2c11..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/docs/ApiResponse.md +++ /dev/null @@ -1,14 +0,0 @@ -# ApiResponse - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**message** | **String** | | [optional] [default to nothing] -**code** | **Int64** | | [optional] [default to nothing] -**type** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/openapigenerator_petstore_v3/petstore/docs/Category.md b/test/server/openapigenerator_petstore_v3/petstore/docs/Category.md deleted file mode 100644 index e454c3b..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/docs/Category.md +++ /dev/null @@ -1,13 +0,0 @@ -# Category - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**name** | **String** | | [optional] [default to nothing] -**id** | **Int64** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/openapigenerator_petstore_v3/petstore/docs/Order.md b/test/server/openapigenerator_petstore_v3/petstore/docs/Order.md deleted file mode 100644 index 98c1bae..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/docs/Order.md +++ /dev/null @@ -1,17 +0,0 @@ -# Order - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**petId** | **Int64** | | [optional] [default to nothing] -**shipDate** | **ZonedDateTime** | | [optional] [default to nothing] -**status** | **String** | Order Status | [optional] [default to nothing] -**id** | **Int64** | | [optional] [default to nothing] -**complete** | **Bool** | | [optional] [default to false] -**quantity** | **Int64** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/openapigenerator_petstore_v3/petstore/docs/Pet.md b/test/server/openapigenerator_petstore_v3/petstore/docs/Pet.md deleted file mode 100644 index dbdd2db..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/docs/Pet.md +++ /dev/null @@ -1,17 +0,0 @@ -# Pet - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**name** | **String** | | [default to nothing] -**status** | **String** | pet status in the store | [optional] [default to nothing] -**id** | **Int64** | | [optional] [default to nothing] -**photoUrls** | **Vector{String}** | | [default to nothing] -**tags** | [**Vector{Tag}**](Tag.md) | | [optional] [default to nothing] -**category** | [***Category**](Category.md) | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/openapigenerator_petstore_v3/petstore/docs/PetApi.md b/test/server/openapigenerator_petstore_v3/petstore/docs/PetApi.md deleted file mode 100644 index 655746e..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/docs/PetApi.md +++ /dev/null @@ -1,268 +0,0 @@ -# PetApi - -All URIs are relative to */v3* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**add_pet**](PetApi.md#add_pet) | **POST** /pet | Add a new pet to the store -[**delete_pet**](PetApi.md#delete_pet) | **DELETE** /pet/{petId} | Deletes a pet -[**find_pets_by_status**](PetApi.md#find_pets_by_status) | **GET** /pet/findByStatus | Finds Pets by status -[**find_pets_by_tags**](PetApi.md#find_pets_by_tags) | **GET** /pet/findByTags | Finds Pets by tags -[**get_pet_by_id**](PetApi.md#get_pet_by_id) | **GET** /pet/{petId} | Find pet by ID -[**update_pet**](PetApi.md#update_pet) | **PUT** /pet | Update an existing pet -[**update_pet_with_form**](PetApi.md#update_pet_with_form) | **POST** /pet/{petId} | Updates a pet in the store with form data -[**upload_file**](PetApi.md#upload_file) | **POST** /pet/{petId}/uploadImage | uploads an image - - -# **add_pet** -> add_pet(req::HTTP.Request, pet::Pet;) -> Pet - -Add a new pet to the store - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**pet** | [**Pet**](Pet.md)| Pet object that needs to be added to the store | - -### Return type - -[**Pet**](Pet.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: application/xml, application/json - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **delete_pet** -> delete_pet(req::HTTP.Request, pet_id::Int64; api_key=nothing,) -> Nothing - -Deletes a pet - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**pet_id** | **Int64**| Pet id to delete | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **api_key** | **String**| | [default to nothing] - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **find_pets_by_status** -> find_pets_by_status(req::HTTP.Request, status::Vector{String};) -> Vector{Pet} - -Finds Pets by status - -Multiple status values can be provided with comma separated strings - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**status** | [**Vector{String}**](String.md)| Status values that need to be considered for filter | - -### Return type - -[**Vector{Pet}**](Pet.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **find_pets_by_tags** -> find_pets_by_tags(req::HTTP.Request, tags::Vector{String};) -> Vector{Pet} - -Finds Pets by tags - -Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**tags** | [**Vector{String}**](String.md)| Tags to filter by | - -### Return type - -[**Vector{Pet}**](Pet.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **get_pet_by_id** -> get_pet_by_id(req::HTTP.Request, pet_id::Int64;) -> Pet - -Find pet by ID - -Returns a single pet - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**pet_id** | **Int64**| ID of pet to return | - -### Return type - -[**Pet**](Pet.md) - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **update_pet** -> update_pet(req::HTTP.Request, pet::Pet;) -> Pet - -Update an existing pet - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**pet** | [**Pet**](Pet.md)| Pet object that needs to be added to the store | - -### Return type - -[**Pet**](Pet.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: application/xml, application/json - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **update_pet_with_form** -> update_pet_with_form(req::HTTP.Request, pet_id::Int64; name=nothing, status=nothing,) -> Nothing - -Updates a pet in the store with form data - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**pet_id** | **Int64**| ID of pet that needs to be updated | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **name** | **String**| Updated name of the pet | [default to nothing] - **status** | **String**| Updated status of the pet | [default to nothing] - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: application/x-www-form-urlencoded - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **upload_file** -> upload_file(req::HTTP.Request, pet_id::Int64; file=nothing, additional_metadata=nothing,) -> ApiResponse - -uploads an image - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**pet_id** | **Int64**| ID of pet to update | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **file** | **Vector{UInt8}**| file to upload | - **additional_metadata** | **String**| Additional data to pass to server | [default to nothing] - -### Return type - -[**ApiResponse**](ApiResponse.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: multipart/form-data - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/test/server/openapigenerator_petstore_v3/petstore/docs/StoreApi.md b/test/server/openapigenerator_petstore_v3/petstore/docs/StoreApi.md deleted file mode 100644 index e64ce54..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/docs/StoreApi.md +++ /dev/null @@ -1,124 +0,0 @@ -# StoreApi - -All URIs are relative to */v3* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**delete_order**](StoreApi.md#delete_order) | **DELETE** /store/order/{orderId} | Delete purchase order by ID -[**get_inventory**](StoreApi.md#get_inventory) | **GET** /store/inventory | Returns pet inventories by status -[**get_order_by_id**](StoreApi.md#get_order_by_id) | **GET** /store/order/{orderId} | Find purchase order by ID -[**place_order**](StoreApi.md#place_order) | **POST** /store/order | Place an order for a pet - - -# **delete_order** -> delete_order(req::HTTP.Request, order_id::String;) -> Nothing - -Delete purchase order by ID - -For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**order_id** | **String**| ID of the order that needs to be deleted | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **get_inventory** -> get_inventory(req::HTTP.Request;) -> Dict{String, Int64} - -Returns pet inventories by status - -Returns a map of status codes to quantities - -### Required Parameters -This endpoint does not need any parameter. - -### Return type - -**Dict{String, Int64}** - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **get_order_by_id** -> get_order_by_id(req::HTTP.Request, order_id::Int64;) -> Order - -Find purchase order by ID - -For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**order_id** | **Int64**| ID of pet that needs to be fetched | - -### Return type - -[**Order**](Order.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **place_order** -> place_order(req::HTTP.Request, order::Order;) -> Order - -Place an order for a pet - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**order** | [**Order**](Order.md)| order placed for purchasing the pet | - -### Return type - -[**Order**](Order.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/test/server/openapigenerator_petstore_v3/petstore/docs/Tag.md b/test/server/openapigenerator_petstore_v3/petstore/docs/Tag.md deleted file mode 100644 index ee2633c..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/docs/Tag.md +++ /dev/null @@ -1,13 +0,0 @@ -# Tag - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**name** | **String** | | [optional] [default to nothing] -**id** | **Int64** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/openapigenerator_petstore_v3/petstore/docs/User.md b/test/server/openapigenerator_petstore_v3/petstore/docs/User.md deleted file mode 100644 index 3db4060..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/docs/User.md +++ /dev/null @@ -1,19 +0,0 @@ -# User - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**password** | **String** | | [optional] [default to nothing] -**id** | **Int64** | | [optional] [default to nothing] -**username** | **String** | | [optional] [default to nothing] -**firstName** | **String** | | [optional] [default to nothing] -**lastName** | **String** | | [optional] [default to nothing] -**phone** | **String** | | [optional] [default to nothing] -**userStatus** | **Int64** | User Status | [optional] [default to nothing] -**email** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/openapigenerator_petstore_v3/petstore/docs/UserApi.md b/test/server/openapigenerator_petstore_v3/petstore/docs/UserApi.md deleted file mode 100644 index 49afb18..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/docs/UserApi.md +++ /dev/null @@ -1,246 +0,0 @@ -# UserApi - -All URIs are relative to */v3* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**create_user**](UserApi.md#create_user) | **POST** /user | Create user -[**create_users_with_array_input**](UserApi.md#create_users_with_array_input) | **POST** /user/createWithArray | Creates list of users with given input array -[**create_users_with_list_input**](UserApi.md#create_users_with_list_input) | **POST** /user/createWithList | Creates list of users with given input array -[**delete_user**](UserApi.md#delete_user) | **DELETE** /user/{username} | Delete user -[**get_user_by_name**](UserApi.md#get_user_by_name) | **GET** /user/{username} | Get user by user name -[**login_user**](UserApi.md#login_user) | **GET** /user/login | Logs user into the system -[**logout_user**](UserApi.md#logout_user) | **GET** /user/logout | Logs out current logged in user session -[**update_user**](UserApi.md#update_user) | **PUT** /user/{username} | Updated user - - -# **create_user** -> create_user(req::HTTP.Request, user::User;) -> Nothing - -Create user - -This can only be done by the logged in user. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**user** | [**User**](User.md)| Created user object | - -### Return type - -Nothing - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **create_users_with_array_input** -> create_users_with_array_input(req::HTTP.Request, user::Vector{User};) -> Nothing - -Creates list of users with given input array - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**user** | [**Vector{User}**](User.md)| List of user object | - -### Return type - -Nothing - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **create_users_with_list_input** -> create_users_with_list_input(req::HTTP.Request, user::Vector{User};) -> Nothing - -Creates list of users with given input array - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**user** | [**Vector{User}**](User.md)| List of user object | - -### Return type - -Nothing - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **delete_user** -> delete_user(req::HTTP.Request, username::String;) -> Nothing - -Delete user - -This can only be done by the logged in user. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**username** | **String**| The name that needs to be deleted | - -### Return type - -Nothing - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **get_user_by_name** -> get_user_by_name(req::HTTP.Request, username::String;) -> User - -Get user by user name - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**username** | **String**| The name that needs to be fetched. Use user1 for testing. | - -### Return type - -[**User**](User.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **login_user** -> login_user(req::HTTP.Request, username::String, password::String;) -> String - -Logs user into the system - - - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**username** | **String**| The user name for login | -**password** | **String**| The password for login in clear text | - -### Return type - -**String** - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **logout_user** -> logout_user(req::HTTP.Request;) -> Nothing - -Logs out current logged in user session - - - -### Required Parameters -This endpoint does not need any parameter. - -### Return type - -Nothing - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **update_user** -> update_user(req::HTTP.Request, username::String, user::User;) -> Nothing - -Updated user - -This can only be done by the logged in user. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**username** | **String**| name that need to be deleted | -**user** | [**User**](User.md)| Updated user object | - -### Return type - -Nothing - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/test/server/openapigenerator_petstore_v3/petstore/src/OpenAPIGenPetStoreServer.jl b/test/server/openapigenerator_petstore_v3/petstore/src/OpenAPIGenPetStoreServer.jl deleted file mode 100644 index a68d7d9..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/src/OpenAPIGenPetStoreServer.jl +++ /dev/null @@ -1,123 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw""" -Encapsulates generated server code for OpenAPIGenPetStoreServer - -The following server methods must be implemented: - -- **add_pet** - - *invocation:* POST /pet - - *signature:* add_pet(req::HTTP.Request, pet::Pet;) -> Pet -- **delete_pet** - - *invocation:* DELETE /pet/{petId} - - *signature:* delete_pet(req::HTTP.Request, pet_id::Int64; api_key=nothing,) -> Nothing -- **find_pets_by_status** - - *invocation:* GET /pet/findByStatus - - *signature:* find_pets_by_status(req::HTTP.Request, status::Vector{String};) -> Vector{Pet} -- **find_pets_by_tags** - - *invocation:* GET /pet/findByTags - - *signature:* find_pets_by_tags(req::HTTP.Request, tags::Vector{String};) -> Vector{Pet} -- **get_pet_by_id** - - *invocation:* GET /pet/{petId} - - *signature:* get_pet_by_id(req::HTTP.Request, pet_id::Int64;) -> Pet -- **update_pet** - - *invocation:* PUT /pet - - *signature:* update_pet(req::HTTP.Request, pet::Pet;) -> Pet -- **update_pet_with_form** - - *invocation:* POST /pet/{petId} - - *signature:* update_pet_with_form(req::HTTP.Request, pet_id::Int64; name=nothing, status=nothing,) -> Nothing -- **upload_file** - - *invocation:* POST /pet/{petId}/uploadImage - - *signature:* upload_file(req::HTTP.Request, pet_id::Int64; file=nothing, additional_metadata=nothing,) -> ApiResponse -- **delete_order** - - *invocation:* DELETE /store/order/{orderId} - - *signature:* delete_order(req::HTTP.Request, order_id::String;) -> Nothing -- **get_inventory** - - *invocation:* GET /store/inventory - - *signature:* get_inventory(req::HTTP.Request;) -> Dict{String, Int64} -- **get_order_by_id** - - *invocation:* GET /store/order/{orderId} - - *signature:* get_order_by_id(req::HTTP.Request, order_id::Int64;) -> Order -- **place_order** - - *invocation:* POST /store/order - - *signature:* place_order(req::HTTP.Request, order::Order;) -> Order -- **create_user** - - *invocation:* POST /user - - *signature:* create_user(req::HTTP.Request, user::User;) -> Nothing -- **create_users_with_array_input** - - *invocation:* POST /user/createWithArray - - *signature:* create_users_with_array_input(req::HTTP.Request, user::Vector{User};) -> Nothing -- **create_users_with_list_input** - - *invocation:* POST /user/createWithList - - *signature:* create_users_with_list_input(req::HTTP.Request, user::Vector{User};) -> Nothing -- **delete_user** - - *invocation:* DELETE /user/{username} - - *signature:* delete_user(req::HTTP.Request, username::String;) -> Nothing -- **get_user_by_name** - - *invocation:* GET /user/{username} - - *signature:* get_user_by_name(req::HTTP.Request, username::String;) -> User -- **login_user** - - *invocation:* GET /user/login - - *signature:* login_user(req::HTTP.Request, username::String, password::String;) -> String -- **logout_user** - - *invocation:* GET /user/logout - - *signature:* logout_user(req::HTTP.Request;) -> Nothing -- **update_user** - - *invocation:* PUT /user/{username} - - *signature:* update_user(req::HTTP.Request, username::String, user::User;) -> Nothing -""" -module OpenAPIGenPetStoreServer - -using HTTP -using URIs -using Dates -using TimeZones -using OpenAPI -using OpenAPI.Servers - -const API_VERSION = "1.0.0" - -include("modelincludes.jl") - -include("apis/api_PetApi.jl") -include("apis/api_StoreApi.jl") -include("apis/api_UserApi.jl") - -""" -Register handlers for all APIs in this module in the supplied `Router` instance. - -Paramerets: -- `router`: Router to register handlers in -- `impl`: module that implements the server methods - -Optional parameters: -- `path_prefix`: prefix to be applied to all paths -- `optional_middlewares`: Register one or more optional middlewares to be applied to all requests. - -Optional middlewares can be one or more of: - - `init`: called before the request is processed - - `pre_validation`: called after the request is parsed but before validation - - `pre_invoke`: called after validation but before the handler is invoked - - `post_invoke`: called after the handler is invoked but before the response is sent - -The order in which middlewares are invoked are: -`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` -""" -function register(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - registerPetApi(router, impl; path_prefix=path_prefix, optional_middlewares...) - registerStoreApi(router, impl; path_prefix=path_prefix, optional_middlewares...) - registerUserApi(router, impl; path_prefix=path_prefix, optional_middlewares...) - return router -end - -# export models -export ApiResponse -export Category -export Order -export Pet -export Tag -export User - -end # module OpenAPIGenPetStoreServer diff --git a/test/server/openapigenerator_petstore_v3/petstore/src/apis/api_PetApi.jl b/test/server/openapigenerator_petstore_v3/petstore/src/apis/api_PetApi.jl deleted file mode 100644 index 2701f42..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/src/apis/api_PetApi.jl +++ /dev/null @@ -1,407 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -function add_pet_read(handler) - function add_pet_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["Pet"] = OpenAPI.Servers.to_param_type(Pet, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function add_pet_validate(handler) - function add_pet_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "add_pet" - - n = "Pet" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function add_pet_invoke(impl; post_invoke=nothing) - function add_pet_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.add_pet(req::HTTP.Request, openapi_params["Pet"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function delete_pet_read(handler) - function delete_pet_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["petId"] = OpenAPI.Servers.to_param(Int64, path_params, "petId", required=true, ) - headers = Dict{String,String}(req.headers) - openapi_params["api_key"] = OpenAPI.Servers.to_param(String, headers, "api_key", ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function delete_pet_validate(handler) - function delete_pet_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "delete_pet" - - n = "petId" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "api_key" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function delete_pet_invoke(impl; post_invoke=nothing) - function delete_pet_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.delete_pet(req::HTTP.Request, openapi_params["petId"]; api_key=get(openapi_params, "api_key", nothing),) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function find_pets_by_status_read(handler) - function find_pets_by_status_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - query_params = HTTP.queryparams(URIs.URI(req.target)) - openapi_params["status"] = OpenAPI.Servers.to_param(Vector{String}, query_params, "status", required=true, style="form", is_explode=false) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function find_pets_by_status_validate(handler) - function find_pets_by_status_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "find_pets_by_status" - - n = "status" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function find_pets_by_status_invoke(impl; post_invoke=nothing) - function find_pets_by_status_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.find_pets_by_status(req::HTTP.Request, openapi_params["status"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function find_pets_by_tags_read(handler) - function find_pets_by_tags_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - query_params = HTTP.queryparams(URIs.URI(req.target)) - openapi_params["tags"] = OpenAPI.Servers.to_param(Vector{String}, query_params, "tags", required=true, style="form", is_explode=false) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function find_pets_by_tags_validate(handler) - function find_pets_by_tags_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "find_pets_by_tags" - - n = "tags" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function find_pets_by_tags_invoke(impl; post_invoke=nothing) - function find_pets_by_tags_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.find_pets_by_tags(req::HTTP.Request, openapi_params["tags"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_pet_by_id_read(handler) - function get_pet_by_id_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["petId"] = OpenAPI.Servers.to_param(Int64, path_params, "petId", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_pet_by_id_validate(handler) - function get_pet_by_id_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "get_pet_by_id" - - n = "petId" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function get_pet_by_id_invoke(impl; post_invoke=nothing) - function get_pet_by_id_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_pet_by_id(req::HTTP.Request, openapi_params["petId"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function update_pet_read(handler) - function update_pet_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["Pet"] = OpenAPI.Servers.to_param_type(Pet, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function update_pet_validate(handler) - function update_pet_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "update_pet" - - n = "Pet" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function update_pet_invoke(impl; post_invoke=nothing) - function update_pet_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.update_pet(req::HTTP.Request, openapi_params["Pet"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function update_pet_with_form_read(handler) - function update_pet_with_form_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["petId"] = OpenAPI.Servers.to_param(Int64, path_params, "petId", required=true, ) - ismultipart = false - form_data = ismultipart ? HTTP.parse_multipart_form(req) : HTTP.queryparams(String(copy(req.body))) - openapi_params["name"] = OpenAPI.Servers.to_param(String, form_data, "name"; multipart=ismultipart, isfile=false, ) - openapi_params["status"] = OpenAPI.Servers.to_param(String, form_data, "status"; multipart=ismultipart, isfile=false, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function update_pet_with_form_validate(handler) - function update_pet_with_form_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "update_pet_with_form" - - n = "petId" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "name" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "status" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function update_pet_with_form_invoke(impl; post_invoke=nothing) - function update_pet_with_form_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.update_pet_with_form(req::HTTP.Request, openapi_params["petId"]; name=get(openapi_params, "name", nothing), status=get(openapi_params, "status", nothing),) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function upload_file_read(handler) - function upload_file_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["petId"] = OpenAPI.Servers.to_param(Int64, path_params, "petId", required=true, ) - ismultipart = true - form_data = ismultipart ? HTTP.parse_multipart_form(req) : HTTP.queryparams(String(copy(req.body))) - openapi_params["file"] = OpenAPI.Servers.to_param(Vector{UInt8}, form_data, "file"; multipart=ismultipart, isfile=true, ) - openapi_params["additionalMetadata"] = OpenAPI.Servers.to_param(String, form_data, "additionalMetadata"; multipart=ismultipart, isfile=false, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function upload_file_validate(handler) - function upload_file_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "upload_file" - - n = "petId" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "file" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "additionalMetadata" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function upload_file_invoke(impl; post_invoke=nothing) - function upload_file_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.upload_file(req::HTTP.Request, openapi_params["petId"]; file=get(openapi_params, "file", nothing), additional_metadata=get(openapi_params, "additionalMetadata", nothing),) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - - -function registerPetApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - HTTP.register!(router, "POST", path_prefix * "/pet", OpenAPI.Servers.middleware(impl, add_pet_read, add_pet_validate, add_pet_invoke; optional_middlewares...)) - HTTP.register!(router, "DELETE", path_prefix * "/pet/{petId}", OpenAPI.Servers.middleware(impl, delete_pet_read, delete_pet_validate, delete_pet_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/pet/findByStatus", OpenAPI.Servers.middleware(impl, find_pets_by_status_read, find_pets_by_status_validate, find_pets_by_status_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/pet/findByTags", OpenAPI.Servers.middleware(impl, find_pets_by_tags_read, find_pets_by_tags_validate, find_pets_by_tags_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/pet/{petId}", OpenAPI.Servers.middleware(impl, get_pet_by_id_read, get_pet_by_id_validate, get_pet_by_id_invoke; optional_middlewares...)) - HTTP.register!(router, "PUT", path_prefix * "/pet", OpenAPI.Servers.middleware(impl, update_pet_read, update_pet_validate, update_pet_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/pet/{petId}", OpenAPI.Servers.middleware(impl, update_pet_with_form_read, update_pet_with_form_validate, update_pet_with_form_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/pet/{petId}/uploadImage", OpenAPI.Servers.middleware(impl, upload_file_read, upload_file_validate, upload_file_invoke; optional_middlewares...)) - return router -end diff --git a/test/server/openapigenerator_petstore_v3/petstore/src/apis/api_StoreApi.jl b/test/server/openapigenerator_petstore_v3/petstore/src/apis/api_StoreApi.jl deleted file mode 100644 index 4a8b69f..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/src/apis/api_StoreApi.jl +++ /dev/null @@ -1,157 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -function delete_order_read(handler) - function delete_order_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["orderId"] = OpenAPI.Servers.to_param(String, path_params, "orderId", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function delete_order_validate(handler) - function delete_order_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "delete_order" - - n = "orderId" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function delete_order_invoke(impl; post_invoke=nothing) - function delete_order_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.delete_order(req::HTTP.Request, openapi_params["orderId"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_inventory_read(handler) - function get_inventory_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_inventory_validate(handler) - function get_inventory_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "get_inventory" - - return handler(req) - end -end - -function get_inventory_invoke(impl; post_invoke=nothing) - function get_inventory_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_inventory(req::HTTP.Request;) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_order_by_id_read(handler) - function get_order_by_id_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["orderId"] = OpenAPI.Servers.to_param(Int64, path_params, "orderId", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_order_by_id_validate(handler) - function get_order_by_id_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "get_order_by_id" - - n = "orderId" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - OpenAPI.validate_param(n, op, :maximum, v, 5, false) - OpenAPI.validate_param(n, op, :minimum, v, 1, false) - end - - return handler(req) - end -end - -function get_order_by_id_invoke(impl; post_invoke=nothing) - function get_order_by_id_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_order_by_id(req::HTTP.Request, openapi_params["orderId"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function place_order_read(handler) - function place_order_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["Order"] = OpenAPI.Servers.to_param_type(Order, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function place_order_validate(handler) - function place_order_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "place_order" - - n = "Order" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function place_order_invoke(impl; post_invoke=nothing) - function place_order_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.place_order(req::HTTP.Request, openapi_params["Order"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - - -function registerStoreApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - HTTP.register!(router, "DELETE", path_prefix * "/store/order/{orderId}", OpenAPI.Servers.middleware(impl, delete_order_read, delete_order_validate, delete_order_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/store/inventory", OpenAPI.Servers.middleware(impl, get_inventory_read, get_inventory_validate, get_inventory_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/store/order/{orderId}", OpenAPI.Servers.middleware(impl, get_order_by_id_read, get_order_by_id_validate, get_order_by_id_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/store/order", OpenAPI.Servers.middleware(impl, place_order_read, place_order_validate, place_order_invoke; optional_middlewares...)) - return router -end diff --git a/test/server/openapigenerator_petstore_v3/petstore/src/apis/api_UserApi.jl b/test/server/openapigenerator_petstore_v3/petstore/src/apis/api_UserApi.jl deleted file mode 100644 index 65f5cdd..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/src/apis/api_UserApi.jl +++ /dev/null @@ -1,348 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -function create_user_read(handler) - function create_user_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["User"] = OpenAPI.Servers.to_param_type(User, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function create_user_validate(handler) - function create_user_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "create_user" - - n = "User" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function create_user_invoke(impl; post_invoke=nothing) - function create_user_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.create_user(req::HTTP.Request, openapi_params["User"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function create_users_with_array_input_read(handler) - function create_users_with_array_input_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["User"] = OpenAPI.Servers.to_param_type(Vector{User}, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function create_users_with_array_input_validate(handler) - function create_users_with_array_input_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "create_users_with_array_input" - - n = "User" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function create_users_with_array_input_invoke(impl; post_invoke=nothing) - function create_users_with_array_input_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.create_users_with_array_input(req::HTTP.Request, openapi_params["User"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function create_users_with_list_input_read(handler) - function create_users_with_list_input_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["User"] = OpenAPI.Servers.to_param_type(Vector{User}, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function create_users_with_list_input_validate(handler) - function create_users_with_list_input_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "create_users_with_list_input" - - n = "User" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function create_users_with_list_input_invoke(impl; post_invoke=nothing) - function create_users_with_list_input_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.create_users_with_list_input(req::HTTP.Request, openapi_params["User"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function delete_user_read(handler) - function delete_user_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["username"] = OpenAPI.Servers.to_param(String, path_params, "username", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function delete_user_validate(handler) - function delete_user_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "delete_user" - - n = "username" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function delete_user_invoke(impl; post_invoke=nothing) - function delete_user_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.delete_user(req::HTTP.Request, openapi_params["username"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_user_by_name_read(handler) - function get_user_by_name_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["username"] = OpenAPI.Servers.to_param(String, path_params, "username", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_user_by_name_validate(handler) - function get_user_by_name_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "get_user_by_name" - - n = "username" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function get_user_by_name_invoke(impl; post_invoke=nothing) - function get_user_by_name_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_user_by_name(req::HTTP.Request, openapi_params["username"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function login_user_read(handler) - function login_user_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - query_params = HTTP.queryparams(URIs.URI(req.target)) - openapi_params["username"] = OpenAPI.Servers.to_param(String, query_params, "username", required=true, style="form", is_explode=true) - openapi_params["password"] = OpenAPI.Servers.to_param(String, query_params, "password", required=true, style="form", is_explode=true) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function login_user_validate(handler) - function login_user_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "login_user" - - n = "username" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - OpenAPI.validate_param(n, op, :pattern, v, r"^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$") - end - - n = "password" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function login_user_invoke(impl; post_invoke=nothing) - function login_user_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.login_user(req::HTTP.Request, openapi_params["username"], openapi_params["password"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function logout_user_read(handler) - function logout_user_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function logout_user_validate(handler) - function logout_user_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "logout_user" - - return handler(req) - end -end - -function logout_user_invoke(impl; post_invoke=nothing) - function logout_user_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.logout_user(req::HTTP.Request;) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function update_user_read(handler) - function update_user_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["username"] = OpenAPI.Servers.to_param(String, path_params, "username", required=true, ) - openapi_params["User"] = OpenAPI.Servers.to_param_type(User, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function update_user_validate(handler) - function update_user_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "update_user" - - n = "username" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "User" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function update_user_invoke(impl; post_invoke=nothing) - function update_user_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.update_user(req::HTTP.Request, openapi_params["username"], openapi_params["User"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - - -function registerUserApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - HTTP.register!(router, "POST", path_prefix * "/user", OpenAPI.Servers.middleware(impl, create_user_read, create_user_validate, create_user_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/user/createWithArray", OpenAPI.Servers.middleware(impl, create_users_with_array_input_read, create_users_with_array_input_validate, create_users_with_array_input_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/user/createWithList", OpenAPI.Servers.middleware(impl, create_users_with_list_input_read, create_users_with_list_input_validate, create_users_with_list_input_invoke; optional_middlewares...)) - HTTP.register!(router, "DELETE", path_prefix * "/user/{username}", OpenAPI.Servers.middleware(impl, delete_user_read, delete_user_validate, delete_user_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/user/{username}", OpenAPI.Servers.middleware(impl, get_user_by_name_read, get_user_by_name_validate, get_user_by_name_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/user/login", OpenAPI.Servers.middleware(impl, login_user_read, login_user_validate, login_user_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/user/logout", OpenAPI.Servers.middleware(impl, logout_user_read, logout_user_validate, logout_user_invoke; optional_middlewares...)) - HTTP.register!(router, "PUT", path_prefix * "/user/{username}", OpenAPI.Servers.middleware(impl, update_user_read, update_user_validate, update_user_invoke; optional_middlewares...)) - return router -end diff --git a/test/server/openapigenerator_petstore_v3/petstore/src/modelincludes.jl b/test/server/openapigenerator_petstore_v3/petstore/src/modelincludes.jl deleted file mode 100644 index b3a3db8..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/src/modelincludes.jl +++ /dev/null @@ -1,9 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -include("models/model_ApiResponse.jl") -include("models/model_Category.jl") -include("models/model_Order.jl") -include("models/model_Pet.jl") -include("models/model_Tag.jl") -include("models/model_User.jl") diff --git a/test/server/openapigenerator_petstore_v3/petstore/src/models/model_ApiResponse.jl b/test/server/openapigenerator_petstore_v3/petstore/src/models/model_ApiResponse.jl deleted file mode 100644 index 80cfeac..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/src/models/model_ApiResponse.jl +++ /dev/null @@ -1,50 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""ApiResponse -Describes the result of uploading an image resource - - ApiResponse(; - message=nothing, - code=nothing, - type=nothing, - ) - - - message::String - - code::Int64 - - type::String -""" -Base.@kwdef mutable struct ApiResponse <: OpenAPI.APIModel - message::Union{Nothing, String} = nothing - code::Union{Nothing, Int64} = nothing - type::Union{Nothing, String} = nothing - - function ApiResponse(message, code, type, ) - o = new(message, code, type, ) - OpenAPI.validate_properties(o) - return o - end -end # type ApiResponse - -const _property_types_ApiResponse = Dict{Symbol,String}(Symbol("message")=>"String", Symbol("code")=>"Int64", Symbol("type")=>"String", ) -OpenAPI.property_type(::Type{ ApiResponse }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_ApiResponse[name]))} - -function OpenAPI.check_required(o::ApiResponse) - true -end - -function OpenAPI.validate_properties(o::ApiResponse) - OpenAPI.validate_property(ApiResponse, Symbol("message"), o.message) - OpenAPI.validate_property(ApiResponse, Symbol("code"), o.code) - OpenAPI.validate_property(ApiResponse, Symbol("type"), o.type) -end - -function OpenAPI.validate_property(::Type{ ApiResponse }, name::Symbol, val) - - - if name === Symbol("code") - OpenAPI.validate_param(name, "ApiResponse", :format, val, "int32") - end - -end diff --git a/test/server/openapigenerator_petstore_v3/petstore/src/models/model_Category.jl b/test/server/openapigenerator_petstore_v3/petstore/src/models/model_Category.jl deleted file mode 100644 index 53e027a..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/src/models/model_Category.jl +++ /dev/null @@ -1,48 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Category -A category for a pet - - Category(; - name=nothing, - id=nothing, - ) - - - name::String - - id::Int64 -""" -Base.@kwdef mutable struct Category <: OpenAPI.APIModel - name::Union{Nothing, String} = nothing - id::Union{Nothing, Int64} = nothing - - function Category(name, id, ) - o = new(name, id, ) - OpenAPI.validate_properties(o) - return o - end -end # type Category - -const _property_types_Category = Dict{Symbol,String}(Symbol("name")=>"String", Symbol("id")=>"Int64", ) -OpenAPI.property_type(::Type{ Category }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Category[name]))} - -function OpenAPI.check_required(o::Category) - true -end - -function OpenAPI.validate_properties(o::Category) - OpenAPI.validate_property(Category, Symbol("name"), o.name) - OpenAPI.validate_property(Category, Symbol("id"), o.id) -end - -function OpenAPI.validate_property(::Type{ Category }, name::Symbol, val) - - if name === Symbol("name") - OpenAPI.validate_param(name, "Category", :pattern, val, r"^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$") - end - - if name === Symbol("id") - OpenAPI.validate_param(name, "Category", :format, val, "int64") - end -end diff --git a/test/server/openapigenerator_petstore_v3/petstore/src/models/model_Order.jl b/test/server/openapigenerator_petstore_v3/petstore/src/models/model_Order.jl deleted file mode 100644 index da6c81e..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/src/models/model_Order.jl +++ /dev/null @@ -1,78 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Order -An order for a pets from the pet store - - Order(; - petId=nothing, - shipDate=nothing, - status=nothing, - id=nothing, - complete=false, - quantity=nothing, - ) - - - petId::Int64 - - shipDate::ZonedDateTime - - status::String : Order Status - - id::Int64 - - complete::Bool - - quantity::Int64 -""" -Base.@kwdef mutable struct Order <: OpenAPI.APIModel - petId::Union{Nothing, Int64} = nothing - shipDate::Union{Nothing, ZonedDateTime} = nothing - status::Union{Nothing, String} = nothing - id::Union{Nothing, Int64} = nothing - complete::Union{Nothing, Bool} = false - quantity::Union{Nothing, Int64} = nothing - - function Order(petId, shipDate, status, id, complete, quantity, ) - o = new(petId, shipDate, status, id, complete, quantity, ) - OpenAPI.validate_properties(o) - return o - end -end # type Order - -const _property_types_Order = Dict{Symbol,String}(Symbol("petId")=>"Int64", Symbol("shipDate")=>"ZonedDateTime", Symbol("status")=>"String", Symbol("id")=>"Int64", Symbol("complete")=>"Bool", Symbol("quantity")=>"Int64", ) -OpenAPI.property_type(::Type{ Order }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Order[name]))} - -function OpenAPI.check_required(o::Order) - true -end - -function OpenAPI.validate_properties(o::Order) - OpenAPI.validate_property(Order, Symbol("petId"), o.petId) - OpenAPI.validate_property(Order, Symbol("shipDate"), o.shipDate) - OpenAPI.validate_property(Order, Symbol("status"), o.status) - OpenAPI.validate_property(Order, Symbol("id"), o.id) - OpenAPI.validate_property(Order, Symbol("complete"), o.complete) - OpenAPI.validate_property(Order, Symbol("quantity"), o.quantity) -end - -function OpenAPI.validate_property(::Type{ Order }, name::Symbol, val) - - if name === Symbol("petId") - OpenAPI.validate_param(name, "Order", :format, val, "int64") - end - - if name === Symbol("shipDate") - OpenAPI.validate_param(name, "Order", :format, val, "date-time") - end - - if name === Symbol("status") - OpenAPI.validate_param(name, "Order", :enum, val, ["placed", "approved", "delivered"]) - end - - - if name === Symbol("id") - OpenAPI.validate_param(name, "Order", :format, val, "int64") - end - - - if name === Symbol("quantity") - OpenAPI.validate_param(name, "Order", :format, val, "int32") - end -end diff --git a/test/server/openapigenerator_petstore_v3/petstore/src/models/model_Pet.jl b/test/server/openapigenerator_petstore_v3/petstore/src/models/model_Pet.jl deleted file mode 100644 index 9b30f64..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/src/models/model_Pet.jl +++ /dev/null @@ -1,71 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Pet -A pet for sale in the pet store - - Pet(; - name=nothing, - status=nothing, - id=nothing, - photoUrls=nothing, - tags=nothing, - category=nothing, - ) - - - name::String - - status::String : pet status in the store - - id::Int64 - - photoUrls::Vector{String} - - tags::Vector{Tag} - - category::Category -""" -Base.@kwdef mutable struct Pet <: OpenAPI.APIModel - name::Union{Nothing, String} = nothing - status::Union{Nothing, String} = nothing - id::Union{Nothing, Int64} = nothing - photoUrls::Union{Nothing, Vector{String}} = nothing - tags::Union{Nothing, Vector} = nothing # spec type: Union{ Nothing, Vector{Tag} } - category = nothing # spec type: Union{ Nothing, Category } - - function Pet(name, status, id, photoUrls, tags, category, ) - o = new(name, status, id, photoUrls, tags, category, ) - OpenAPI.validate_properties(o) - return o - end -end # type Pet - -const _property_types_Pet = Dict{Symbol,String}(Symbol("name")=>"String", Symbol("status")=>"String", Symbol("id")=>"Int64", Symbol("photoUrls")=>"Vector{String}", Symbol("tags")=>"Vector{Tag}", Symbol("category")=>"Category", ) -OpenAPI.property_type(::Type{ Pet }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Pet[name]))} - -function OpenAPI.check_required(o::Pet) - o.name === nothing && (return false) - o.photoUrls === nothing && (return false) - true -end - -function OpenAPI.validate_properties(o::Pet) - OpenAPI.validate_property(Pet, Symbol("name"), o.name) - OpenAPI.validate_property(Pet, Symbol("status"), o.status) - OpenAPI.validate_property(Pet, Symbol("id"), o.id) - OpenAPI.validate_property(Pet, Symbol("photoUrls"), o.photoUrls) - OpenAPI.validate_property(Pet, Symbol("tags"), o.tags) - OpenAPI.validate_property(Pet, Symbol("category"), o.category) -end - -function OpenAPI.validate_property(::Type{ Pet }, name::Symbol, val) - - - if name === Symbol("status") - OpenAPI.validate_param(name, "Pet", :enum, val, ["available", "pending", "sold"]) - end - - - if name === Symbol("id") - OpenAPI.validate_param(name, "Pet", :format, val, "int64") - end - - - -end diff --git a/test/server/openapigenerator_petstore_v3/petstore/src/models/model_Tag.jl b/test/server/openapigenerator_petstore_v3/petstore/src/models/model_Tag.jl deleted file mode 100644 index 0550acc..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/src/models/model_Tag.jl +++ /dev/null @@ -1,45 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Tag -A tag for a pet - - Tag(; - name=nothing, - id=nothing, - ) - - - name::String - - id::Int64 -""" -Base.@kwdef mutable struct Tag <: OpenAPI.APIModel - name::Union{Nothing, String} = nothing - id::Union{Nothing, Int64} = nothing - - function Tag(name, id, ) - o = new(name, id, ) - OpenAPI.validate_properties(o) - return o - end -end # type Tag - -const _property_types_Tag = Dict{Symbol,String}(Symbol("name")=>"String", Symbol("id")=>"Int64", ) -OpenAPI.property_type(::Type{ Tag }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Tag[name]))} - -function OpenAPI.check_required(o::Tag) - true -end - -function OpenAPI.validate_properties(o::Tag) - OpenAPI.validate_property(Tag, Symbol("name"), o.name) - OpenAPI.validate_property(Tag, Symbol("id"), o.id) -end - -function OpenAPI.validate_property(::Type{ Tag }, name::Symbol, val) - - - if name === Symbol("id") - OpenAPI.validate_param(name, "Tag", :format, val, "int64") - end -end diff --git a/test/server/openapigenerator_petstore_v3/petstore/src/models/model_User.jl b/test/server/openapigenerator_petstore_v3/petstore/src/models/model_User.jl deleted file mode 100644 index 95e836a..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore/src/models/model_User.jl +++ /dev/null @@ -1,78 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""User -A User who is purchasing from the pet store - - User(; - password=nothing, - id=nothing, - username=nothing, - firstName=nothing, - lastName=nothing, - phone=nothing, - userStatus=nothing, - email=nothing, - ) - - - password::String - - id::Int64 - - username::String - - firstName::String - - lastName::String - - phone::String - - userStatus::Int64 : User Status - - email::String -""" -Base.@kwdef mutable struct User <: OpenAPI.APIModel - password::Union{Nothing, String} = nothing - id::Union{Nothing, Int64} = nothing - username::Union{Nothing, String} = nothing - firstName::Union{Nothing, String} = nothing - lastName::Union{Nothing, String} = nothing - phone::Union{Nothing, String} = nothing - userStatus::Union{Nothing, Int64} = nothing - email::Union{Nothing, String} = nothing - - function User(password, id, username, firstName, lastName, phone, userStatus, email, ) - o = new(password, id, username, firstName, lastName, phone, userStatus, email, ) - OpenAPI.validate_properties(o) - return o - end -end # type User - -const _property_types_User = Dict{Symbol,String}(Symbol("password")=>"String", Symbol("id")=>"Int64", Symbol("username")=>"String", Symbol("firstName")=>"String", Symbol("lastName")=>"String", Symbol("phone")=>"String", Symbol("userStatus")=>"Int64", Symbol("email")=>"String", ) -OpenAPI.property_type(::Type{ User }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_User[name]))} - -function OpenAPI.check_required(o::User) - true -end - -function OpenAPI.validate_properties(o::User) - OpenAPI.validate_property(User, Symbol("password"), o.password) - OpenAPI.validate_property(User, Symbol("id"), o.id) - OpenAPI.validate_property(User, Symbol("username"), o.username) - OpenAPI.validate_property(User, Symbol("firstName"), o.firstName) - OpenAPI.validate_property(User, Symbol("lastName"), o.lastName) - OpenAPI.validate_property(User, Symbol("phone"), o.phone) - OpenAPI.validate_property(User, Symbol("userStatus"), o.userStatus) - OpenAPI.validate_property(User, Symbol("email"), o.email) -end - -function OpenAPI.validate_property(::Type{ User }, name::Symbol, val) - - - if name === Symbol("id") - OpenAPI.validate_param(name, "User", :format, val, "int64") - end - - - - - - if name === Symbol("userStatus") - OpenAPI.validate_param(name, "User", :format, val, "int32") - end - -end diff --git a/test/server/openapigenerator_petstore_v3/petstore_server.jl b/test/server/openapigenerator_petstore_v3/petstore_server.jl deleted file mode 100644 index 3ee5ca7..0000000 --- a/test/server/openapigenerator_petstore_v3/petstore_server.jl +++ /dev/null @@ -1,169 +0,0 @@ -module OpenAPIGenPetStoreV3Server - -using HTTP - -include("petstore/src/OpenAPIGenPetStoreServer.jl") - -using .OpenAPIGenPetStoreServer - -const server = Ref{Any}(nothing) -const pets = Vector{Pet}() -const orders = Vector{Order}() -const users = Vector{User}() -const PRESET_TEST_USER = "user1" - -function add_pet(req::HTTP.Request, pet::Pet;) - push!(pets, pet) - return pet -end - -function delete_pet(req::HTTP.Request, pet_id::Int64; api_key=nothing,) - filter!(x->x.id != pet_id, pets) - return nothing -end - -function find_pets_by_status(req::HTTP.Request, status::Vector{String};) - return filter(x->x.status == status, pets) -end - -function find_pets_by_tags(req::HTTP.Request, tags::Vector{String};) - return filter(x->!isempty(intersect(Set(x.tags), Set(tags))), pets) -end - -function get_pet_by_id(req::HTTP.Request, pet_id::Int64;) - pet = findfirst(x->x.id == pet_id, pets) - if pet === nothing - return HTTP.Response(404, "Pet not found") - else - return pets[pet] - end -end - -function update_pet(req::HTTP.Request, pet::Pet;) - filter!(x->x.id != pet.id, pets) - push!(pets, pet) - return pet -end - -function update_pet_with_form(req::HTTP.Request, pet_id::Int64; name=nothing, status=nothing,) - for pet in pets - if pet.id == pet_id - if !isnothing(name) - pet.name = name - end - if !isnothing(status) - pet.status = status - end - end - end - return nothing -end - -function upload_file(req::HTTP.Request, pet_id::Int64; additional_metadata=nothing, file=nothing,) - return ApiResponse(; code=1, type="pet", message="file uploaded", ) -end - -function delete_order(req::HTTP.Request, order_id::String;) - filter!(x->x.id != order_id, orders) - return nothing -end - -function get_inventory(req::HTTP.Request;) - return Dict{String, Int64}( - "additionalProp1" => 0, - "additionalProp2" => 0, - "additionalProp3" => 0, - ) -end - -function get_order_by_id(req::HTTP.Request, order_id::Int64;) - order = findfirst(x->x.id == order_id, orders) - if order === nothing - return HTTP.Response(404, "Order not found") - else - return orders[order] - end -end - -function place_order(req::HTTP.Request, order::Order;) - if isnothing(order.id) - max_OrderId = isempty(orders) ? 0 : maximum(x->x.id, orders) - order.id = max_OrderId + 1 - end - push!(orders, order) - return order -end - -function create_user(req::HTTP.Request, user::User;) - push!(users, user) - return nothing -end - -function create_users_with_array_input(req::HTTP.Request, user::Vector{User};) - append!(users, user) - return nothing -end - -function create_users_with_list_input(req::HTTP.Request, user::Vector{User};) - append!(users, user) - return nothing -end - -function delete_user(req::HTTP.Request, username::String;) - filter!(x->x.username != username, users) - return nothing -end - -function get_user_by_name(req::HTTP.Request, username::String;) - # user = findfirst(x->x.username == username, users) - # if user === nothing - # return HTTP.Response(404, "User not found") - # else - # return user - # end - if username == PRESET_TEST_USER - return User(; id=1, username=PRESET_TEST_USER, firstName="John", lastName="Doe", email="jondoe@test.com", phone="1234567890", userStatus=1, ) - else - return HTTP.Response(404, "User not found") - end -end - -function login_user(req::HTTP.Request, username::String, password::String;) - return "logged in user session: test" -end - -function logout_user(req::HTTP.Request;) - return nothing -end - -function update_user(req::HTTP.Request, username::String, user::User;) - filter!(x->x.username != username, users) - push!(users, user) - return nothing -end - -function stop(::HTTP.Request) - HTTP.close(server[]) - return HTTP.Response(200, "") -end - -function ping(::HTTP.Request) - return HTTP.Response(200, "") -end - -function run_server(port=8081) - try - router = HTTP.Router() - router = OpenAPIGenPetStoreServer.register(router, @__MODULE__; path_prefix="/v3") - HTTP.register!(router, "GET", "/stop", stop) - HTTP.register!(router, "GET", "/ping", ping) - server[] = HTTP.serve!(router, port) - wait(server[]) - catch ex - @error("Server error", exception=(ex, catch_backtrace())) - end -end - -end # module OpenAPIGenPetStoreV3Server - -OpenAPIGenPetStoreV3Server.run_server() diff --git a/test/server/petstore_v2/generate.sh b/test/server/petstore_v2/generate.sh deleted file mode 100755 index 11211d6..0000000 --- a/test/server/petstore_v2/generate.sh +++ /dev/null @@ -1,6 +0,0 @@ -java -jar openapi-generator-cli.jar generate \ - -i ../../specs/petstore_v2.json \ - -g julia-server \ - -o petstore \ - --additional-properties=packageName=PetStoreServer \ - --additional-properties=exportModels=true diff --git a/test/server/petstore_v2/petstore/.openapi-generator-ignore b/test/server/petstore_v2/petstore/.openapi-generator-ignore deleted file mode 100644 index 7484ee5..0000000 --- a/test/server/petstore_v2/petstore/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/test/server/petstore_v2/petstore/.openapi-generator/FILES b/test/server/petstore_v2/petstore/.openapi-generator/FILES deleted file mode 100644 index d9189bb..0000000 --- a/test/server/petstore_v2/petstore/.openapi-generator/FILES +++ /dev/null @@ -1,21 +0,0 @@ -README.md -docs/ApiResponse.md -docs/Category.md -docs/Order.md -docs/Pet.md -docs/PetApi.md -docs/StoreApi.md -docs/Tag.md -docs/User.md -docs/UserApi.md -src/PetStoreServer.jl -src/apis/api_PetApi.jl -src/apis/api_StoreApi.jl -src/apis/api_UserApi.jl -src/modelincludes.jl -src/models/model_ApiResponse.jl -src/models/model_Category.jl -src/models/model_Order.jl -src/models/model_Pet.jl -src/models/model_Tag.jl -src/models/model_User.jl diff --git a/test/server/petstore_v2/petstore/.openapi-generator/VERSION b/test/server/petstore_v2/petstore/.openapi-generator/VERSION deleted file mode 100644 index 96cfbb1..0000000 --- a/test/server/petstore_v2/petstore/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.13.0-SNAPSHOT diff --git a/test/server/petstore_v2/petstore/README.md b/test/server/petstore_v2/petstore/README.md deleted file mode 100644 index dfaddfe..0000000 --- a/test/server/petstore_v2/petstore/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# Julia API server for PetStoreServer - -This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. - -## Overview -This API server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. - -- API version: 1.0.6 -- Generator version: 7.13.0-SNAPSHOT -- Build package: org.openapitools.codegen.languages.JuliaServerCodegen - - -## Installation -Place the Julia files generated under the `src` folder in your Julia project. Include PetStoreServer.jl in the project code. -It would include the module named PetStoreServer. - -Implement the server methods as listed below. They are also documented with the PetStoreServer module. -Launch a HTTP server with a router that has all handlers registered. A `register` method is provided in PetStoreServer module for convenience. - -```julia -register( - router::HTTP.Router, # Router to register handlers in - impl; # Module that implements the server methods - path_prefix::String="", # Prefix to be applied to all paths - optional_middlewares... # Optional middlewares to be applied to all handlers -) -``` - -Optional middlewares can be one or more of: -- `init`: called before the request is processed -- `pre_validation`: called after the request is parsed but before validation -- `pre_invoke`: called after validation but before the handler is invoked -- `post_invoke`: called after the handler is invoked but before the response is sent - -The order in which middlewares are invoked are: -`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` - - -## API Endpoints - -The following server methods must be implemented: - -Class | Method | HTTP request | Description ------------- | ------------- | ------------- | ------------- -*PetApi* | [**add_pet**](docs/PetApi.md#add_pet) | **POST** /pet | Add a new pet to the store -*PetApi* | [**delete_pet**](docs/PetApi.md#delete_pet) | **DELETE** /pet/{petId} | Deletes a pet -*PetApi* | [**find_pets_by_status**](docs/PetApi.md#find_pets_by_status) | **GET** /pet/findByStatus | Finds Pets by status -*PetApi* | [**find_pets_by_tags**](docs/PetApi.md#find_pets_by_tags) | **GET** /pet/findByTags | Finds Pets by tags -*PetApi* | [**get_pet_by_id**](docs/PetApi.md#get_pet_by_id) | **GET** /pet/{petId} | Find pet by ID -*PetApi* | [**update_pet**](docs/PetApi.md#update_pet) | **PUT** /pet | Update an existing pet -*PetApi* | [**update_pet_with_form**](docs/PetApi.md#update_pet_with_form) | **POST** /pet/{petId} | Updates a pet in the store with form data -*PetApi* | [**upload_file**](docs/PetApi.md#upload_file) | **POST** /pet/{petId}/uploadImage | uploads an image -*StoreApi* | [**delete_order**](docs/StoreApi.md#delete_order) | **DELETE** /store/order/{orderId} | Delete purchase order by ID -*StoreApi* | [**get_inventory**](docs/StoreApi.md#get_inventory) | **GET** /store/inventory | Returns pet inventories by status -*StoreApi* | [**get_order_by_id**](docs/StoreApi.md#get_order_by_id) | **GET** /store/order/{orderId} | Find purchase order by ID -*StoreApi* | [**place_order**](docs/StoreApi.md#place_order) | **POST** /store/order | Place an order for a pet -*UserApi* | [**create_user**](docs/UserApi.md#create_user) | **POST** /user | Create user -*UserApi* | [**create_users_with_array_input**](docs/UserApi.md#create_users_with_array_input) | **POST** /user/createWithArray | Creates list of users with given input array -*UserApi* | [**create_users_with_list_input**](docs/UserApi.md#create_users_with_list_input) | **POST** /user/createWithList | Creates list of users with given input array -*UserApi* | [**delete_user**](docs/UserApi.md#delete_user) | **DELETE** /user/{username} | Delete user -*UserApi* | [**get_user_by_name**](docs/UserApi.md#get_user_by_name) | **GET** /user/{username} | Get user by user name -*UserApi* | [**login_user**](docs/UserApi.md#login_user) | **GET** /user/login | Logs user into the system -*UserApi* | [**logout_user**](docs/UserApi.md#logout_user) | **GET** /user/logout | Logs out current logged in user session -*UserApi* | [**update_user**](docs/UserApi.md#update_user) | **PUT** /user/{username} | Updated user - - - -## Models - - - [ApiResponse](docs/ApiResponse.md) - - [Category](docs/Category.md) - - [Order](docs/Order.md) - - [Pet](docs/Pet.md) - - [Tag](docs/Tag.md) - - [User](docs/User.md) - - - -## Author - -apiteam@swagger.io - diff --git a/test/server/petstore_v2/petstore/docs/ApiResponse.md b/test/server/petstore_v2/petstore/docs/ApiResponse.md deleted file mode 100644 index 664dd24..0000000 --- a/test/server/petstore_v2/petstore/docs/ApiResponse.md +++ /dev/null @@ -1,14 +0,0 @@ -# ApiResponse - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**code** | **Int64** | | [optional] [default to nothing] -**type** | **String** | | [optional] [default to nothing] -**message** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/petstore_v2/petstore/docs/Category.md b/test/server/petstore_v2/petstore/docs/Category.md deleted file mode 100644 index 4e93290..0000000 --- a/test/server/petstore_v2/petstore/docs/Category.md +++ /dev/null @@ -1,13 +0,0 @@ -# Category - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**name** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/petstore_v2/petstore/docs/Order.md b/test/server/petstore_v2/petstore/docs/Order.md deleted file mode 100644 index d94aa08..0000000 --- a/test/server/petstore_v2/petstore/docs/Order.md +++ /dev/null @@ -1,17 +0,0 @@ -# Order - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**petId** | **Int64** | | [optional] [default to nothing] -**quantity** | **Int64** | | [optional] [default to nothing] -**shipDate** | **ZonedDateTime** | | [optional] [default to nothing] -**status** | **String** | Order Status | [optional] [default to nothing] -**complete** | **Bool** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/petstore_v2/petstore/docs/Pet.md b/test/server/petstore_v2/petstore/docs/Pet.md deleted file mode 100644 index a8bba4c..0000000 --- a/test/server/petstore_v2/petstore/docs/Pet.md +++ /dev/null @@ -1,17 +0,0 @@ -# Pet - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**category** | [***Category**](Category.md) | | [optional] [default to nothing] -**name** | **String** | | [default to nothing] -**photoUrls** | **Vector{String}** | | [default to nothing] -**tags** | [**Vector{Tag}**](Tag.md) | | [optional] [default to nothing] -**status** | **String** | pet status in the store | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/petstore_v2/petstore/docs/PetApi.md b/test/server/petstore_v2/petstore/docs/PetApi.md deleted file mode 100644 index 291445d..0000000 --- a/test/server/petstore_v2/petstore/docs/PetApi.md +++ /dev/null @@ -1,258 +0,0 @@ -# PetApi - -All URIs are relative to *https://petstore.swagger.io/v2* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**add_pet**](PetApi.md#add_pet) | **POST** /pet | Add a new pet to the store -[**delete_pet**](PetApi.md#delete_pet) | **DELETE** /pet/{petId} | Deletes a pet -[**find_pets_by_status**](PetApi.md#find_pets_by_status) | **GET** /pet/findByStatus | Finds Pets by status -[**find_pets_by_tags**](PetApi.md#find_pets_by_tags) | **GET** /pet/findByTags | Finds Pets by tags -[**get_pet_by_id**](PetApi.md#get_pet_by_id) | **GET** /pet/{petId} | Find pet by ID -[**update_pet**](PetApi.md#update_pet) | **PUT** /pet | Update an existing pet -[**update_pet_with_form**](PetApi.md#update_pet_with_form) | **POST** /pet/{petId} | Updates a pet in the store with form data -[**upload_file**](PetApi.md#upload_file) | **POST** /pet/{petId}/uploadImage | uploads an image - - -# **add_pet** -> add_pet(req::HTTP.Request, body::Pet;) -> Nothing - -Add a new pet to the store - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**body** | [**Pet**](Pet.md)| Pet object that needs to be added to the store | - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: application/json, application/xml - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **delete_pet** -> delete_pet(req::HTTP.Request, pet_id::Int64; api_key=nothing,) -> Nothing - -Deletes a pet - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**pet_id** | **Int64**| Pet id to delete | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **api_key** | **String**| | [default to nothing] - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **find_pets_by_status** -> find_pets_by_status(req::HTTP.Request, status::Vector{String};) -> Vector{Pet} - -Finds Pets by status - -Multiple status values can be provided with comma separated strings - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**status** | [**Vector{String}**](String.md)| Status values that need to be considered for filter | - -### Return type - -[**Vector{Pet}**](Pet.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json, application/xml - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **find_pets_by_tags** -> find_pets_by_tags(req::HTTP.Request, tags::Vector{String};) -> Vector{Pet} - -Finds Pets by tags - -Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**tags** | [**Vector{String}**](String.md)| Tags to filter by | - -### Return type - -[**Vector{Pet}**](Pet.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json, application/xml - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **get_pet_by_id** -> get_pet_by_id(req::HTTP.Request, pet_id::Int64;) -> Pet - -Find pet by ID - -Returns a single pet - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**pet_id** | **Int64**| ID of pet to return | - -### Return type - -[**Pet**](Pet.md) - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json, application/xml - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **update_pet** -> update_pet(req::HTTP.Request, body::Pet;) -> Nothing - -Update an existing pet - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**body** | [**Pet**](Pet.md)| Pet object that needs to be added to the store | - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: application/json, application/xml - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **update_pet_with_form** -> update_pet_with_form(req::HTTP.Request, pet_id::Int64; name=nothing, status=nothing,) -> Nothing - -Updates a pet in the store with form data - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**pet_id** | **Int64**| ID of pet that needs to be updated | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **name** | **String**| Updated name of the pet | [default to nothing] - **status** | **String**| Updated status of the pet | [default to nothing] - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: application/x-www-form-urlencoded - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **upload_file** -> upload_file(req::HTTP.Request, pet_id::Int64; additional_metadata=nothing, file=nothing,) -> ApiResponse - -uploads an image - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**pet_id** | **Int64**| ID of pet to update | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **additional_metadata** | **String**| Additional data to pass to server | [default to nothing] - **file** | **Vector{UInt8}**| file to upload | - -### Return type - -[**ApiResponse**](ApiResponse.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: multipart/form-data - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/test/server/petstore_v2/petstore/docs/StoreApi.md b/test/server/petstore_v2/petstore/docs/StoreApi.md deleted file mode 100644 index 6a9b9fd..0000000 --- a/test/server/petstore_v2/petstore/docs/StoreApi.md +++ /dev/null @@ -1,122 +0,0 @@ -# StoreApi - -All URIs are relative to *https://petstore.swagger.io/v2* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**delete_order**](StoreApi.md#delete_order) | **DELETE** /store/order/{orderId} | Delete purchase order by ID -[**get_inventory**](StoreApi.md#get_inventory) | **GET** /store/inventory | Returns pet inventories by status -[**get_order_by_id**](StoreApi.md#get_order_by_id) | **GET** /store/order/{orderId} | Find purchase order by ID -[**place_order**](StoreApi.md#place_order) | **POST** /store/order | Place an order for a pet - - -# **delete_order** -> delete_order(req::HTTP.Request, order_id::Int64;) -> Nothing - -Delete purchase order by ID - -For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**order_id** | **Int64**| ID of the order that needs to be deleted | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **get_inventory** -> get_inventory(req::HTTP.Request;) -> Dict{String, Int64} - -Returns pet inventories by status - -Returns a map of status codes to quantities - -### Required Parameters -This endpoint does not need any parameter. - -### Return type - -**Dict{String, Int64}** - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **get_order_by_id** -> get_order_by_id(req::HTTP.Request, order_id::Int64;) -> Order - -Find purchase order by ID - -For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**order_id** | **Int64**| ID of pet that needs to be fetched | - -### Return type - -[**Order**](Order.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json, application/xml - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **place_order** -> place_order(req::HTTP.Request, body::Order;) -> Order - -Place an order for a pet - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**body** | [**Order**](Order.md)| order placed for purchasing the pet | - -### Return type - -[**Order**](Order.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json, application/xml - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/test/server/petstore_v2/petstore/docs/Tag.md b/test/server/petstore_v2/petstore/docs/Tag.md deleted file mode 100644 index c904872..0000000 --- a/test/server/petstore_v2/petstore/docs/Tag.md +++ /dev/null @@ -1,13 +0,0 @@ -# Tag - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**name** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/petstore_v2/petstore/docs/User.md b/test/server/petstore_v2/petstore/docs/User.md deleted file mode 100644 index 5318b5a..0000000 --- a/test/server/petstore_v2/petstore/docs/User.md +++ /dev/null @@ -1,19 +0,0 @@ -# User - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**username** | **String** | | [optional] [default to nothing] -**firstName** | **String** | | [optional] [default to nothing] -**lastName** | **String** | | [optional] [default to nothing] -**email** | **String** | | [optional] [default to nothing] -**password** | **String** | | [optional] [default to nothing] -**phone** | **String** | | [optional] [default to nothing] -**userStatus** | **Int64** | User Status | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/petstore_v2/petstore/docs/UserApi.md b/test/server/petstore_v2/petstore/docs/UserApi.md deleted file mode 100644 index 7f21ab5..0000000 --- a/test/server/petstore_v2/petstore/docs/UserApi.md +++ /dev/null @@ -1,236 +0,0 @@ -# UserApi - -All URIs are relative to *https://petstore.swagger.io/v2* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**create_user**](UserApi.md#create_user) | **POST** /user | Create user -[**create_users_with_array_input**](UserApi.md#create_users_with_array_input) | **POST** /user/createWithArray | Creates list of users with given input array -[**create_users_with_list_input**](UserApi.md#create_users_with_list_input) | **POST** /user/createWithList | Creates list of users with given input array -[**delete_user**](UserApi.md#delete_user) | **DELETE** /user/{username} | Delete user -[**get_user_by_name**](UserApi.md#get_user_by_name) | **GET** /user/{username} | Get user by user name -[**login_user**](UserApi.md#login_user) | **GET** /user/login | Logs user into the system -[**logout_user**](UserApi.md#logout_user) | **GET** /user/logout | Logs out current logged in user session -[**update_user**](UserApi.md#update_user) | **PUT** /user/{username} | Updated user - - -# **create_user** -> create_user(req::HTTP.Request, body::User;) -> Nothing - -Create user - -This can only be done by the logged in user. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**body** | [**User**](User.md)| Created user object | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **create_users_with_array_input** -> create_users_with_array_input(req::HTTP.Request, body::Vector{User};) -> Nothing - -Creates list of users with given input array - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**body** | [**Vector{User}**](User.md)| List of user object | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **create_users_with_list_input** -> create_users_with_list_input(req::HTTP.Request, body::Vector{User};) -> Nothing - -Creates list of users with given input array - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**body** | [**Vector{User}**](User.md)| List of user object | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **delete_user** -> delete_user(req::HTTP.Request, username::String;) -> Nothing - -Delete user - -This can only be done by the logged in user. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**username** | **String**| The name that needs to be deleted | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **get_user_by_name** -> get_user_by_name(req::HTTP.Request, username::String;) -> User - -Get user by user name - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**username** | **String**| The name that needs to be fetched. Use user1 for testing. | - -### Return type - -[**User**](User.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json, application/xml - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **login_user** -> login_user(req::HTTP.Request, username::String, password::String;) -> String - -Logs user into the system - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**username** | **String**| The user name for login | -**password** | **String**| The password for login in clear text | - -### Return type - -**String** - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json, application/xml - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **logout_user** -> logout_user(req::HTTP.Request;) -> Nothing - -Logs out current logged in user session - -### Required Parameters -This endpoint does not need any parameter. - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **update_user** -> update_user(req::HTTP.Request, username::String, body::User;) -> Nothing - -Updated user - -This can only be done by the logged in user. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**username** | **String**| name that need to be updated | -**body** | [**User**](User.md)| Updated user object | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/test/server/petstore_v2/petstore/src/PetStoreServer.jl b/test/server/petstore_v2/petstore/src/PetStoreServer.jl deleted file mode 100644 index 841bc62..0000000 --- a/test/server/petstore_v2/petstore/src/PetStoreServer.jl +++ /dev/null @@ -1,123 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw""" -Encapsulates generated server code for PetStoreServer - -The following server methods must be implemented: - -- **add_pet** - - *invocation:* POST /pet - - *signature:* add_pet(req::HTTP.Request, body::Pet;) -> Nothing -- **delete_pet** - - *invocation:* DELETE /pet/{petId} - - *signature:* delete_pet(req::HTTP.Request, pet_id::Int64; api_key=nothing,) -> Nothing -- **find_pets_by_status** - - *invocation:* GET /pet/findByStatus - - *signature:* find_pets_by_status(req::HTTP.Request, status::Vector{String};) -> Vector{Pet} -- **find_pets_by_tags** - - *invocation:* GET /pet/findByTags - - *signature:* find_pets_by_tags(req::HTTP.Request, tags::Vector{String};) -> Vector{Pet} -- **get_pet_by_id** - - *invocation:* GET /pet/{petId} - - *signature:* get_pet_by_id(req::HTTP.Request, pet_id::Int64;) -> Pet -- **update_pet** - - *invocation:* PUT /pet - - *signature:* update_pet(req::HTTP.Request, body::Pet;) -> Nothing -- **update_pet_with_form** - - *invocation:* POST /pet/{petId} - - *signature:* update_pet_with_form(req::HTTP.Request, pet_id::Int64; name=nothing, status=nothing,) -> Nothing -- **upload_file** - - *invocation:* POST /pet/{petId}/uploadImage - - *signature:* upload_file(req::HTTP.Request, pet_id::Int64; additional_metadata=nothing, file=nothing,) -> ApiResponse -- **delete_order** - - *invocation:* DELETE /store/order/{orderId} - - *signature:* delete_order(req::HTTP.Request, order_id::Int64;) -> Nothing -- **get_inventory** - - *invocation:* GET /store/inventory - - *signature:* get_inventory(req::HTTP.Request;) -> Dict{String, Int64} -- **get_order_by_id** - - *invocation:* GET /store/order/{orderId} - - *signature:* get_order_by_id(req::HTTP.Request, order_id::Int64;) -> Order -- **place_order** - - *invocation:* POST /store/order - - *signature:* place_order(req::HTTP.Request, body::Order;) -> Order -- **create_user** - - *invocation:* POST /user - - *signature:* create_user(req::HTTP.Request, body::User;) -> Nothing -- **create_users_with_array_input** - - *invocation:* POST /user/createWithArray - - *signature:* create_users_with_array_input(req::HTTP.Request, body::Vector{User};) -> Nothing -- **create_users_with_list_input** - - *invocation:* POST /user/createWithList - - *signature:* create_users_with_list_input(req::HTTP.Request, body::Vector{User};) -> Nothing -- **delete_user** - - *invocation:* DELETE /user/{username} - - *signature:* delete_user(req::HTTP.Request, username::String;) -> Nothing -- **get_user_by_name** - - *invocation:* GET /user/{username} - - *signature:* get_user_by_name(req::HTTP.Request, username::String;) -> User -- **login_user** - - *invocation:* GET /user/login - - *signature:* login_user(req::HTTP.Request, username::String, password::String;) -> String -- **logout_user** - - *invocation:* GET /user/logout - - *signature:* logout_user(req::HTTP.Request;) -> Nothing -- **update_user** - - *invocation:* PUT /user/{username} - - *signature:* update_user(req::HTTP.Request, username::String, body::User;) -> Nothing -""" -module PetStoreServer - -using HTTP -using URIs -using Dates -using TimeZones -using OpenAPI -using OpenAPI.Servers - -const API_VERSION = "1.0.6" - -include("modelincludes.jl") - -include("apis/api_PetApi.jl") -include("apis/api_StoreApi.jl") -include("apis/api_UserApi.jl") - -""" -Register handlers for all APIs in this module in the supplied `Router` instance. - -Paramerets: -- `router`: Router to register handlers in -- `impl`: module that implements the server methods - -Optional parameters: -- `path_prefix`: prefix to be applied to all paths -- `optional_middlewares`: Register one or more optional middlewares to be applied to all requests. - -Optional middlewares can be one or more of: - - `init`: called before the request is processed - - `pre_validation`: called after the request is parsed but before validation - - `pre_invoke`: called after validation but before the handler is invoked - - `post_invoke`: called after the handler is invoked but before the response is sent - -The order in which middlewares are invoked are: -`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` -""" -function register(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - registerPetApi(router, impl; path_prefix=path_prefix, optional_middlewares...) - registerStoreApi(router, impl; path_prefix=path_prefix, optional_middlewares...) - registerUserApi(router, impl; path_prefix=path_prefix, optional_middlewares...) - return router -end - -# export models -export ApiResponse -export Category -export Order -export Pet -export Tag -export User - -end # module PetStoreServer diff --git a/test/server/petstore_v2/petstore/src/apis/api_PetApi.jl b/test/server/petstore_v2/petstore/src/apis/api_PetApi.jl deleted file mode 100644 index 9bccaf3..0000000 --- a/test/server/petstore_v2/petstore/src/apis/api_PetApi.jl +++ /dev/null @@ -1,407 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -function add_pet_read(handler) - function add_pet_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["body"] = OpenAPI.Servers.to_param_type(Pet, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function add_pet_validate(handler) - function add_pet_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "add_pet" - - n = "body" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function add_pet_invoke(impl; post_invoke=nothing) - function add_pet_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.add_pet(req::HTTP.Request, openapi_params["body"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function delete_pet_read(handler) - function delete_pet_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["petId"] = OpenAPI.Servers.to_param(Int64, path_params, "petId", required=true, ) - headers = Dict{String,String}(req.headers) - openapi_params["api_key"] = OpenAPI.Servers.to_param(String, headers, "api_key", ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function delete_pet_validate(handler) - function delete_pet_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "delete_pet" - - n = "petId" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "api_key" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function delete_pet_invoke(impl; post_invoke=nothing) - function delete_pet_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.delete_pet(req::HTTP.Request, openapi_params["petId"]; api_key=get(openapi_params, "api_key", nothing),) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function find_pets_by_status_read(handler) - function find_pets_by_status_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - query_params = HTTP.queryparams(URIs.URI(req.target)) - openapi_params["status"] = OpenAPI.Servers.to_param(Vector{String}, query_params, "status", required=true, style="form", is_explode=true) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function find_pets_by_status_validate(handler) - function find_pets_by_status_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "find_pets_by_status" - - n = "status" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function find_pets_by_status_invoke(impl; post_invoke=nothing) - function find_pets_by_status_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.find_pets_by_status(req::HTTP.Request, openapi_params["status"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function find_pets_by_tags_read(handler) - function find_pets_by_tags_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - query_params = HTTP.queryparams(URIs.URI(req.target)) - openapi_params["tags"] = OpenAPI.Servers.to_param(Vector{String}, query_params, "tags", required=true, style="form", is_explode=true) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function find_pets_by_tags_validate(handler) - function find_pets_by_tags_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "find_pets_by_tags" - - n = "tags" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function find_pets_by_tags_invoke(impl; post_invoke=nothing) - function find_pets_by_tags_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.find_pets_by_tags(req::HTTP.Request, openapi_params["tags"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_pet_by_id_read(handler) - function get_pet_by_id_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["petId"] = OpenAPI.Servers.to_param(Int64, path_params, "petId", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_pet_by_id_validate(handler) - function get_pet_by_id_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "get_pet_by_id" - - n = "petId" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function get_pet_by_id_invoke(impl; post_invoke=nothing) - function get_pet_by_id_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_pet_by_id(req::HTTP.Request, openapi_params["petId"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function update_pet_read(handler) - function update_pet_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["body"] = OpenAPI.Servers.to_param_type(Pet, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function update_pet_validate(handler) - function update_pet_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "update_pet" - - n = "body" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function update_pet_invoke(impl; post_invoke=nothing) - function update_pet_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.update_pet(req::HTTP.Request, openapi_params["body"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function update_pet_with_form_read(handler) - function update_pet_with_form_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["petId"] = OpenAPI.Servers.to_param(Int64, path_params, "petId", required=true, ) - ismultipart = false - form_data = ismultipart ? HTTP.parse_multipart_form(req) : HTTP.queryparams(String(copy(req.body))) - openapi_params["name"] = OpenAPI.Servers.to_param(String, form_data, "name"; multipart=ismultipart, isfile=false, ) - openapi_params["status"] = OpenAPI.Servers.to_param(String, form_data, "status"; multipart=ismultipart, isfile=false, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function update_pet_with_form_validate(handler) - function update_pet_with_form_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "update_pet_with_form" - - n = "petId" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "name" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "status" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function update_pet_with_form_invoke(impl; post_invoke=nothing) - function update_pet_with_form_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.update_pet_with_form(req::HTTP.Request, openapi_params["petId"]; name=get(openapi_params, "name", nothing), status=get(openapi_params, "status", nothing),) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function upload_file_read(handler) - function upload_file_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["petId"] = OpenAPI.Servers.to_param(Int64, path_params, "petId", required=true, ) - ismultipart = true - form_data = ismultipart ? HTTP.parse_multipart_form(req) : HTTP.queryparams(String(copy(req.body))) - openapi_params["additionalMetadata"] = OpenAPI.Servers.to_param(String, form_data, "additionalMetadata"; multipart=ismultipart, isfile=false, ) - openapi_params["file"] = OpenAPI.Servers.to_param(Vector{UInt8}, form_data, "file"; multipart=ismultipart, isfile=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function upload_file_validate(handler) - function upload_file_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "upload_file" - - n = "petId" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "additionalMetadata" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "file" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function upload_file_invoke(impl; post_invoke=nothing) - function upload_file_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.upload_file(req::HTTP.Request, openapi_params["petId"]; additional_metadata=get(openapi_params, "additionalMetadata", nothing), file=get(openapi_params, "file", nothing),) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - - -function registerPetApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - HTTP.register!(router, "POST", path_prefix * "/pet", OpenAPI.Servers.middleware(impl, add_pet_read, add_pet_validate, add_pet_invoke; optional_middlewares...)) - HTTP.register!(router, "DELETE", path_prefix * "/pet/{petId}", OpenAPI.Servers.middleware(impl, delete_pet_read, delete_pet_validate, delete_pet_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/pet/findByStatus", OpenAPI.Servers.middleware(impl, find_pets_by_status_read, find_pets_by_status_validate, find_pets_by_status_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/pet/findByTags", OpenAPI.Servers.middleware(impl, find_pets_by_tags_read, find_pets_by_tags_validate, find_pets_by_tags_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/pet/{petId}", OpenAPI.Servers.middleware(impl, get_pet_by_id_read, get_pet_by_id_validate, get_pet_by_id_invoke; optional_middlewares...)) - HTTP.register!(router, "PUT", path_prefix * "/pet", OpenAPI.Servers.middleware(impl, update_pet_read, update_pet_validate, update_pet_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/pet/{petId}", OpenAPI.Servers.middleware(impl, update_pet_with_form_read, update_pet_with_form_validate, update_pet_with_form_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/pet/{petId}/uploadImage", OpenAPI.Servers.middleware(impl, upload_file_read, upload_file_validate, upload_file_invoke; optional_middlewares...)) - return router -end diff --git a/test/server/petstore_v2/petstore/src/apis/api_StoreApi.jl b/test/server/petstore_v2/petstore/src/apis/api_StoreApi.jl deleted file mode 100644 index 9482b0c..0000000 --- a/test/server/petstore_v2/petstore/src/apis/api_StoreApi.jl +++ /dev/null @@ -1,152 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -function delete_order_read(handler) - function delete_order_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["orderId"] = OpenAPI.Servers.to_param(Int64, path_params, "orderId", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function delete_order_validate(handler) - function delete_order_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "delete_order" - - n = "orderId" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - OpenAPI.validate_param(n, op, :minimum, v, 1, false) - end - - return handler(req) - end -end - -function delete_order_invoke(impl; post_invoke=nothing) - function delete_order_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.delete_order(req::HTTP.Request, openapi_params["orderId"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_inventory_read(handler) - function get_inventory_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_inventory_validate(handler) - function get_inventory_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "get_inventory" - - return handler(req) - end -end - -function get_inventory_invoke(impl; post_invoke=nothing) - function get_inventory_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_inventory(req::HTTP.Request;) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_order_by_id_read(handler) - function get_order_by_id_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["orderId"] = OpenAPI.Servers.to_param(Int64, path_params, "orderId", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_order_by_id_validate(handler) - function get_order_by_id_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "get_order_by_id" - - n = "orderId" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - OpenAPI.validate_param(n, op, :maximum, v, 10, false) - OpenAPI.validate_param(n, op, :minimum, v, 1, false) - end - - return handler(req) - end -end - -function get_order_by_id_invoke(impl; post_invoke=nothing) - function get_order_by_id_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_order_by_id(req::HTTP.Request, openapi_params["orderId"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function place_order_read(handler) - function place_order_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["body"] = OpenAPI.Servers.to_param_type(Order, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function place_order_validate(handler) - function place_order_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "place_order" - - n = "body" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function place_order_invoke(impl; post_invoke=nothing) - function place_order_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.place_order(req::HTTP.Request, openapi_params["body"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - - -function registerStoreApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - HTTP.register!(router, "DELETE", path_prefix * "/store/order/{orderId}", OpenAPI.Servers.middleware(impl, delete_order_read, delete_order_validate, delete_order_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/store/inventory", OpenAPI.Servers.middleware(impl, get_inventory_read, get_inventory_validate, get_inventory_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/store/order/{orderId}", OpenAPI.Servers.middleware(impl, get_order_by_id_read, get_order_by_id_validate, get_order_by_id_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/store/order", OpenAPI.Servers.middleware(impl, place_order_read, place_order_validate, place_order_invoke; optional_middlewares...)) - return router -end diff --git a/test/server/petstore_v2/petstore/src/apis/api_UserApi.jl b/test/server/petstore_v2/petstore/src/apis/api_UserApi.jl deleted file mode 100644 index 5d4ffed..0000000 --- a/test/server/petstore_v2/petstore/src/apis/api_UserApi.jl +++ /dev/null @@ -1,353 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -function create_user_read(handler) - function create_user_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["body"] = OpenAPI.Servers.to_param_type(User, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function create_user_validate(handler) - function create_user_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "create_user" - - n = "body" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function create_user_invoke(impl; post_invoke=nothing) - function create_user_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.create_user(req::HTTP.Request, openapi_params["body"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function create_users_with_array_input_read(handler) - function create_users_with_array_input_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["body"] = OpenAPI.Servers.to_param_type(Vector{User}, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function create_users_with_array_input_validate(handler) - function create_users_with_array_input_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "create_users_with_array_input" - - n = "body" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function create_users_with_array_input_invoke(impl; post_invoke=nothing) - function create_users_with_array_input_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.create_users_with_array_input(req::HTTP.Request, openapi_params["body"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function create_users_with_list_input_read(handler) - function create_users_with_list_input_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["body"] = OpenAPI.Servers.to_param_type(Vector{User}, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function create_users_with_list_input_validate(handler) - function create_users_with_list_input_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "create_users_with_list_input" - - n = "body" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function create_users_with_list_input_invoke(impl; post_invoke=nothing) - function create_users_with_list_input_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.create_users_with_list_input(req::HTTP.Request, openapi_params["body"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function delete_user_read(handler) - function delete_user_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["username"] = OpenAPI.Servers.to_param(String, path_params, "username", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function delete_user_validate(handler) - function delete_user_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "delete_user" - - n = "username" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function delete_user_invoke(impl; post_invoke=nothing) - function delete_user_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.delete_user(req::HTTP.Request, openapi_params["username"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_user_by_name_read(handler) - function get_user_by_name_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["username"] = OpenAPI.Servers.to_param(String, path_params, "username", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_user_by_name_validate(handler) - function get_user_by_name_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "get_user_by_name" - - n = "username" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function get_user_by_name_invoke(impl; post_invoke=nothing) - function get_user_by_name_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_user_by_name(req::HTTP.Request, openapi_params["username"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function login_user_read(handler) - function login_user_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - query_params = HTTP.queryparams(URIs.URI(req.target)) - openapi_params["username"] = OpenAPI.Servers.to_param(String, query_params, "username", required=true, style="", is_explode=false) - openapi_params["password"] = OpenAPI.Servers.to_param(String, query_params, "password", required=true, style="", is_explode=false) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function login_user_validate(handler) - function login_user_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "login_user" - - n = "username" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "password" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function login_user_invoke(impl; post_invoke=nothing) - function login_user_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.login_user(req::HTTP.Request, openapi_params["username"], openapi_params["password"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function logout_user_read(handler) - function logout_user_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function logout_user_validate(handler) - function logout_user_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "logout_user" - - return handler(req) - end -end - -function logout_user_invoke(impl; post_invoke=nothing) - function logout_user_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.logout_user(req::HTTP.Request;) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function update_user_read(handler) - function update_user_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["username"] = OpenAPI.Servers.to_param(String, path_params, "username", required=true, ) - openapi_params["body"] = OpenAPI.Servers.to_param_type(User, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function update_user_validate(handler) - function update_user_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "update_user" - - n = "username" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "body" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function update_user_invoke(impl; post_invoke=nothing) - function update_user_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.update_user(req::HTTP.Request, openapi_params["username"], openapi_params["body"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - - -function registerUserApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - HTTP.register!(router, "POST", path_prefix * "/user", OpenAPI.Servers.middleware(impl, create_user_read, create_user_validate, create_user_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/user/createWithArray", OpenAPI.Servers.middleware(impl, create_users_with_array_input_read, create_users_with_array_input_validate, create_users_with_array_input_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/user/createWithList", OpenAPI.Servers.middleware(impl, create_users_with_list_input_read, create_users_with_list_input_validate, create_users_with_list_input_invoke; optional_middlewares...)) - HTTP.register!(router, "DELETE", path_prefix * "/user/{username}", OpenAPI.Servers.middleware(impl, delete_user_read, delete_user_validate, delete_user_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/user/{username}", OpenAPI.Servers.middleware(impl, get_user_by_name_read, get_user_by_name_validate, get_user_by_name_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/user/login", OpenAPI.Servers.middleware(impl, login_user_read, login_user_validate, login_user_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/user/logout", OpenAPI.Servers.middleware(impl, logout_user_read, logout_user_validate, logout_user_invoke; optional_middlewares...)) - HTTP.register!(router, "PUT", path_prefix * "/user/{username}", OpenAPI.Servers.middleware(impl, update_user_read, update_user_validate, update_user_invoke; optional_middlewares...)) - return router -end diff --git a/test/server/petstore_v2/petstore/src/modelincludes.jl b/test/server/petstore_v2/petstore/src/modelincludes.jl deleted file mode 100644 index b3a3db8..0000000 --- a/test/server/petstore_v2/petstore/src/modelincludes.jl +++ /dev/null @@ -1,9 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -include("models/model_ApiResponse.jl") -include("models/model_Category.jl") -include("models/model_Order.jl") -include("models/model_Pet.jl") -include("models/model_Tag.jl") -include("models/model_User.jl") diff --git a/test/server/petstore_v2/petstore/src/models/model_ApiResponse.jl b/test/server/petstore_v2/petstore/src/models/model_ApiResponse.jl deleted file mode 100644 index a196ba0..0000000 --- a/test/server/petstore_v2/petstore/src/models/model_ApiResponse.jl +++ /dev/null @@ -1,49 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""ApiResponse - - ApiResponse(; - code=nothing, - type=nothing, - message=nothing, - ) - - - code::Int64 - - type::String - - message::String -""" -Base.@kwdef mutable struct ApiResponse <: OpenAPI.APIModel - code::Union{Nothing, Int64} = nothing - type::Union{Nothing, String} = nothing - message::Union{Nothing, String} = nothing - - function ApiResponse(code, type, message, ) - o = new(code, type, message, ) - OpenAPI.validate_properties(o) - return o - end -end # type ApiResponse - -const _property_types_ApiResponse = Dict{Symbol,String}(Symbol("code")=>"Int64", Symbol("type")=>"String", Symbol("message")=>"String", ) -OpenAPI.property_type(::Type{ ApiResponse }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_ApiResponse[name]))} - -function OpenAPI.check_required(o::ApiResponse) - true -end - -function OpenAPI.validate_properties(o::ApiResponse) - OpenAPI.validate_property(ApiResponse, Symbol("code"), o.code) - OpenAPI.validate_property(ApiResponse, Symbol("type"), o.type) - OpenAPI.validate_property(ApiResponse, Symbol("message"), o.message) -end - -function OpenAPI.validate_property(::Type{ ApiResponse }, name::Symbol, val) - - if name === Symbol("code") - OpenAPI.validate_param(name, "ApiResponse", :format, val, "int32") - end - - -end diff --git a/test/server/petstore_v2/petstore/src/models/model_Category.jl b/test/server/petstore_v2/petstore/src/models/model_Category.jl deleted file mode 100644 index 159890d..0000000 --- a/test/server/petstore_v2/petstore/src/models/model_Category.jl +++ /dev/null @@ -1,44 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Category - - Category(; - id=nothing, - name=nothing, - ) - - - id::Int64 - - name::String -""" -Base.@kwdef mutable struct Category <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - name::Union{Nothing, String} = nothing - - function Category(id, name, ) - o = new(id, name, ) - OpenAPI.validate_properties(o) - return o - end -end # type Category - -const _property_types_Category = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("name")=>"String", ) -OpenAPI.property_type(::Type{ Category }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Category[name]))} - -function OpenAPI.check_required(o::Category) - true -end - -function OpenAPI.validate_properties(o::Category) - OpenAPI.validate_property(Category, Symbol("id"), o.id) - OpenAPI.validate_property(Category, Symbol("name"), o.name) -end - -function OpenAPI.validate_property(::Type{ Category }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "Category", :format, val, "int64") - end - -end diff --git a/test/server/petstore_v2/petstore/src/models/model_Order.jl b/test/server/petstore_v2/petstore/src/models/model_Order.jl deleted file mode 100644 index a11ca51..0000000 --- a/test/server/petstore_v2/petstore/src/models/model_Order.jl +++ /dev/null @@ -1,77 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Order - - Order(; - id=nothing, - petId=nothing, - quantity=nothing, - shipDate=nothing, - status=nothing, - complete=nothing, - ) - - - id::Int64 - - petId::Int64 - - quantity::Int64 - - shipDate::ZonedDateTime - - status::String : Order Status - - complete::Bool -""" -Base.@kwdef mutable struct Order <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - petId::Union{Nothing, Int64} = nothing - quantity::Union{Nothing, Int64} = nothing - shipDate::Union{Nothing, ZonedDateTime} = nothing - status::Union{Nothing, String} = nothing - complete::Union{Nothing, Bool} = nothing - - function Order(id, petId, quantity, shipDate, status, complete, ) - o = new(id, petId, quantity, shipDate, status, complete, ) - OpenAPI.validate_properties(o) - return o - end -end # type Order - -const _property_types_Order = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("petId")=>"Int64", Symbol("quantity")=>"Int64", Symbol("shipDate")=>"ZonedDateTime", Symbol("status")=>"String", Symbol("complete")=>"Bool", ) -OpenAPI.property_type(::Type{ Order }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Order[name]))} - -function OpenAPI.check_required(o::Order) - true -end - -function OpenAPI.validate_properties(o::Order) - OpenAPI.validate_property(Order, Symbol("id"), o.id) - OpenAPI.validate_property(Order, Symbol("petId"), o.petId) - OpenAPI.validate_property(Order, Symbol("quantity"), o.quantity) - OpenAPI.validate_property(Order, Symbol("shipDate"), o.shipDate) - OpenAPI.validate_property(Order, Symbol("status"), o.status) - OpenAPI.validate_property(Order, Symbol("complete"), o.complete) -end - -function OpenAPI.validate_property(::Type{ Order }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "Order", :format, val, "int64") - end - - if name === Symbol("petId") - OpenAPI.validate_param(name, "Order", :format, val, "int64") - end - - if name === Symbol("quantity") - OpenAPI.validate_param(name, "Order", :format, val, "int32") - end - - if name === Symbol("shipDate") - OpenAPI.validate_param(name, "Order", :format, val, "date-time") - end - - if name === Symbol("status") - OpenAPI.validate_param(name, "Order", :enum, val, ["placed", "approved", "delivered"]) - end - - -end diff --git a/test/server/petstore_v2/petstore/src/models/model_Pet.jl b/test/server/petstore_v2/petstore/src/models/model_Pet.jl deleted file mode 100644 index 322d93a..0000000 --- a/test/server/petstore_v2/petstore/src/models/model_Pet.jl +++ /dev/null @@ -1,70 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Pet - - Pet(; - id=nothing, - category=nothing, - name=nothing, - photoUrls=nothing, - tags=nothing, - status=nothing, - ) - - - id::Int64 - - category::Category - - name::String - - photoUrls::Vector{String} - - tags::Vector{Tag} - - status::String : pet status in the store -""" -Base.@kwdef mutable struct Pet <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - category = nothing # spec type: Union{ Nothing, Category } - name::Union{Nothing, String} = nothing - photoUrls::Union{Nothing, Vector{String}} = nothing - tags::Union{Nothing, Vector} = nothing # spec type: Union{ Nothing, Vector{Tag} } - status::Union{Nothing, String} = nothing - - function Pet(id, category, name, photoUrls, tags, status, ) - o = new(id, category, name, photoUrls, tags, status, ) - OpenAPI.validate_properties(o) - return o - end -end # type Pet - -const _property_types_Pet = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("category")=>"Category", Symbol("name")=>"String", Symbol("photoUrls")=>"Vector{String}", Symbol("tags")=>"Vector{Tag}", Symbol("status")=>"String", ) -OpenAPI.property_type(::Type{ Pet }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Pet[name]))} - -function OpenAPI.check_required(o::Pet) - o.name === nothing && (return false) - o.photoUrls === nothing && (return false) - true -end - -function OpenAPI.validate_properties(o::Pet) - OpenAPI.validate_property(Pet, Symbol("id"), o.id) - OpenAPI.validate_property(Pet, Symbol("category"), o.category) - OpenAPI.validate_property(Pet, Symbol("name"), o.name) - OpenAPI.validate_property(Pet, Symbol("photoUrls"), o.photoUrls) - OpenAPI.validate_property(Pet, Symbol("tags"), o.tags) - OpenAPI.validate_property(Pet, Symbol("status"), o.status) -end - -function OpenAPI.validate_property(::Type{ Pet }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "Pet", :format, val, "int64") - end - - - - - - if name === Symbol("status") - OpenAPI.validate_param(name, "Pet", :enum, val, ["available", "pending", "sold"]) - end - -end diff --git a/test/server/petstore_v2/petstore/src/models/model_Tag.jl b/test/server/petstore_v2/petstore/src/models/model_Tag.jl deleted file mode 100644 index 83051f1..0000000 --- a/test/server/petstore_v2/petstore/src/models/model_Tag.jl +++ /dev/null @@ -1,44 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Tag - - Tag(; - id=nothing, - name=nothing, - ) - - - id::Int64 - - name::String -""" -Base.@kwdef mutable struct Tag <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - name::Union{Nothing, String} = nothing - - function Tag(id, name, ) - o = new(id, name, ) - OpenAPI.validate_properties(o) - return o - end -end # type Tag - -const _property_types_Tag = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("name")=>"String", ) -OpenAPI.property_type(::Type{ Tag }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Tag[name]))} - -function OpenAPI.check_required(o::Tag) - true -end - -function OpenAPI.validate_properties(o::Tag) - OpenAPI.validate_property(Tag, Symbol("id"), o.id) - OpenAPI.validate_property(Tag, Symbol("name"), o.name) -end - -function OpenAPI.validate_property(::Type{ Tag }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "Tag", :format, val, "int64") - end - -end diff --git a/test/server/petstore_v2/petstore/src/models/model_User.jl b/test/server/petstore_v2/petstore/src/models/model_User.jl deleted file mode 100644 index 39b2359..0000000 --- a/test/server/petstore_v2/petstore/src/models/model_User.jl +++ /dev/null @@ -1,77 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""User - - User(; - id=nothing, - username=nothing, - firstName=nothing, - lastName=nothing, - email=nothing, - password=nothing, - phone=nothing, - userStatus=nothing, - ) - - - id::Int64 - - username::String - - firstName::String - - lastName::String - - email::String - - password::String - - phone::String - - userStatus::Int64 : User Status -""" -Base.@kwdef mutable struct User <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - username::Union{Nothing, String} = nothing - firstName::Union{Nothing, String} = nothing - lastName::Union{Nothing, String} = nothing - email::Union{Nothing, String} = nothing - password::Union{Nothing, String} = nothing - phone::Union{Nothing, String} = nothing - userStatus::Union{Nothing, Int64} = nothing - - function User(id, username, firstName, lastName, email, password, phone, userStatus, ) - o = new(id, username, firstName, lastName, email, password, phone, userStatus, ) - OpenAPI.validate_properties(o) - return o - end -end # type User - -const _property_types_User = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("username")=>"String", Symbol("firstName")=>"String", Symbol("lastName")=>"String", Symbol("email")=>"String", Symbol("password")=>"String", Symbol("phone")=>"String", Symbol("userStatus")=>"Int64", ) -OpenAPI.property_type(::Type{ User }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_User[name]))} - -function OpenAPI.check_required(o::User) - true -end - -function OpenAPI.validate_properties(o::User) - OpenAPI.validate_property(User, Symbol("id"), o.id) - OpenAPI.validate_property(User, Symbol("username"), o.username) - OpenAPI.validate_property(User, Symbol("firstName"), o.firstName) - OpenAPI.validate_property(User, Symbol("lastName"), o.lastName) - OpenAPI.validate_property(User, Symbol("email"), o.email) - OpenAPI.validate_property(User, Symbol("password"), o.password) - OpenAPI.validate_property(User, Symbol("phone"), o.phone) - OpenAPI.validate_property(User, Symbol("userStatus"), o.userStatus) -end - -function OpenAPI.validate_property(::Type{ User }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "User", :format, val, "int64") - end - - - - - - - - if name === Symbol("userStatus") - OpenAPI.validate_param(name, "User", :format, val, "int32") - end -end diff --git a/test/server/petstore_v2/petstore_server.jl b/test/server/petstore_v2/petstore_server.jl deleted file mode 100644 index 91d5e0a..0000000 --- a/test/server/petstore_v2/petstore_server.jl +++ /dev/null @@ -1,170 +0,0 @@ -module PetStoreV2Server - -using HTTP -using JSON - -include("petstore/src/PetStoreServer.jl") - -using .PetStoreServer - -const server = Ref{Any}(nothing) -const pets = Vector{Pet}() -const orders = Vector{Order}() -const users = Vector{User}() -const PRESET_TEST_USER = "user1" - -function add_pet(req::HTTP.Request, pet::Pet;) - push!(pets, pet) - return nothing -end - -function delete_pet(req::HTTP.Request, pet_id::Int64; api_key=nothing,) - filter!(x->x.id != pet_id, pets) - return nothing -end - -function find_pets_by_status(req::HTTP.Request, status::Vector{String};) - return filter(x->x.status == status, pets) -end - -function find_pets_by_tags(req::HTTP.Request, tags::Vector{String};) - return filter(x->!isempty(intersect(Set(x.tags), Set(tags))), pets) -end - -function get_pet_by_id(req::HTTP.Request, pet_id::Int64;) - pet = findfirst(x->x.id == pet_id, pets) - if pet === nothing - return HTTP.Response(404, "Pet not found") - else - return pets[pet] - end -end - -function update_pet(req::HTTP.Request, pet::Pet;) - filter!(x->x.id != pet.id, pets) - push!(pets, pet) - return nothing -end - -function update_pet_with_form(req::HTTP.Request, pet_id::Int64; name=nothing, status=nothing,) - for pet in pets - if pet.id == pet_id - if !isnothing(name) - pet.name = name - end - if !isnothing(status) - pet.status = status - end - end - end - return nothing -end - -function upload_file(req::HTTP.Request, pet_id::Int64; additional_metadata=nothing, file=nothing,) - return ApiResponse(; code=1, type="pet", message="file uploaded", ) -end - -function delete_order(req::HTTP.Request, order_id::Int64;) - filter!(x->x.id != order_id, orders) - return nothing -end - -function get_inventory(req::HTTP.Request;) - return Dict{String, Int64}( - "additionalProp1" => 0, - "additionalProp2" => 0, - "additionalProp3" => 0, - ) -end - -function get_order_by_id(req::HTTP.Request, order_id::Int64;) - order = findfirst(x->x.id == order_id, orders) - if order === nothing - return HTTP.Response(404, "Order not found") - else - return orders[order] - end -end - -function place_order(req::HTTP.Request, order::Order;) - if isnothing(order.id) - max_OrderId = isempty(orders) ? 0 : maximum(x->x.id, orders) - order.id = max_OrderId + 1 - end - push!(orders, order) - return order -end - -function create_user(req::HTTP.Request, user::User;) - push!(users, user) - return nothing -end - -function create_users_with_array_input(req::HTTP.Request, user::Vector{User};) - append!(users, user) - return nothing -end - -function create_users_with_list_input(req::HTTP.Request, user::Vector{User};) - append!(users, user) - return nothing -end - -function delete_user(req::HTTP.Request, username::String;) - filter!(x->x.username != username, users) - return nothing -end - -function get_user_by_name(req::HTTP.Request, username::String;) - # user = findfirst(x->x.username == username, users) - # if user === nothing - # return HTTP.Response(404, "User not found") - # else - # return user - # end - if username == PRESET_TEST_USER - return User(; id=1, username=PRESET_TEST_USER, firstName="John", lastName="Doe", email="jondoe@test.com", phone="1234567890", userStatus=1, ) - else - return HTTP.Response(404, "User not found") - end -end - -function login_user(req::HTTP.Request, username::String, password::String;) - return JSON.json(Dict("message"=>"logged in user session: test", "code"=>200)) -end - -function logout_user(req::HTTP.Request;) - return nothing -end - -function update_user(req::HTTP.Request, username::String, user::User;) - filter!(x->x.username != username, users) - push!(users, user) - return nothing -end - -function stop(::HTTP.Request) - HTTP.close(server[]) - return HTTP.Response(200, "") -end - -function ping(::HTTP.Request) - return HTTP.Response(200, "") -end - -function run_server(port=8080) - try - router = HTTP.Router() - router = PetStoreServer.register(router, @__MODULE__; path_prefix="/v2") - HTTP.register!(router, "GET", "/stop", stop) - HTTP.register!(router, "GET", "/ping", ping) - server[] = HTTP.serve!(router, port) - wait(server[]) - catch ex - @error("Server error", exception=(ex, catch_backtrace())) - end -end - -end # module PetStoreV2Server - -PetStoreV2Server.run_server() \ No newline at end of file diff --git a/test/server/petstore_v3/generate.sh b/test/server/petstore_v3/generate.sh deleted file mode 100755 index f0920d1..0000000 --- a/test/server/petstore_v3/generate.sh +++ /dev/null @@ -1,6 +0,0 @@ -java -jar openapi-generator-cli.jar generate \ - -i ../../specs/petstore_v3.json \ - -g julia-server \ - -o petstore \ - --additional-properties=packageName=PetStoreServer \ - --additional-properties=exportModels=true diff --git a/test/server/petstore_v3/petstore/.openapi-generator-ignore b/test/server/petstore_v3/petstore/.openapi-generator-ignore deleted file mode 100644 index 7484ee5..0000000 --- a/test/server/petstore_v3/petstore/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/test/server/petstore_v3/petstore/.openapi-generator/FILES b/test/server/petstore_v3/petstore/.openapi-generator/FILES deleted file mode 100644 index d9189bb..0000000 --- a/test/server/petstore_v3/petstore/.openapi-generator/FILES +++ /dev/null @@ -1,21 +0,0 @@ -README.md -docs/ApiResponse.md -docs/Category.md -docs/Order.md -docs/Pet.md -docs/PetApi.md -docs/StoreApi.md -docs/Tag.md -docs/User.md -docs/UserApi.md -src/PetStoreServer.jl -src/apis/api_PetApi.jl -src/apis/api_StoreApi.jl -src/apis/api_UserApi.jl -src/modelincludes.jl -src/models/model_ApiResponse.jl -src/models/model_Category.jl -src/models/model_Order.jl -src/models/model_Pet.jl -src/models/model_Tag.jl -src/models/model_User.jl diff --git a/test/server/petstore_v3/petstore/.openapi-generator/VERSION b/test/server/petstore_v3/petstore/.openapi-generator/VERSION deleted file mode 100644 index 96cfbb1..0000000 --- a/test/server/petstore_v3/petstore/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.13.0-SNAPSHOT diff --git a/test/server/petstore_v3/petstore/README.md b/test/server/petstore_v3/petstore/README.md deleted file mode 100644 index b1fc21d..0000000 --- a/test/server/petstore_v3/petstore/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# Julia API server for PetStoreServer - -This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. For OAuth2 flow, you may use `user` as both username and password when asked to login. - -## Overview -This API server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. - -- API version: 1.0.0 -- Generator version: 7.13.0-SNAPSHOT -- Build package: org.openapitools.codegen.languages.JuliaServerCodegen - - -## Installation -Place the Julia files generated under the `src` folder in your Julia project. Include PetStoreServer.jl in the project code. -It would include the module named PetStoreServer. - -Implement the server methods as listed below. They are also documented with the PetStoreServer module. -Launch a HTTP server with a router that has all handlers registered. A `register` method is provided in PetStoreServer module for convenience. - -```julia -register( - router::HTTP.Router, # Router to register handlers in - impl; # Module that implements the server methods - path_prefix::String="", # Prefix to be applied to all paths - optional_middlewares... # Optional middlewares to be applied to all handlers -) -``` - -Optional middlewares can be one or more of: -- `init`: called before the request is processed -- `pre_validation`: called after the request is parsed but before validation -- `pre_invoke`: called after validation but before the handler is invoked -- `post_invoke`: called after the handler is invoked but before the response is sent - -The order in which middlewares are invoked are: -`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` - - -## API Endpoints - -The following server methods must be implemented: - -Class | Method | HTTP request | Description ------------- | ------------- | ------------- | ------------- -*PetApi* | [**add_pet**](docs/PetApi.md#add_pet) | **POST** /pet | Add a new pet to the store -*PetApi* | [**delete_pet**](docs/PetApi.md#delete_pet) | **DELETE** /pet/{petId} | Deletes a pet -*PetApi* | [**find_pets_by_status**](docs/PetApi.md#find_pets_by_status) | **GET** /pet/findByStatus | Finds Pets by status -*PetApi* | [**find_pets_by_tags**](docs/PetApi.md#find_pets_by_tags) | **GET** /pet/findByTags | Finds Pets by tags -*PetApi* | [**get_pet_by_id**](docs/PetApi.md#get_pet_by_id) | **GET** /pet/{petId} | Find pet by ID -*PetApi* | [**update_pet**](docs/PetApi.md#update_pet) | **PUT** /pet | Update an existing pet -*PetApi* | [**update_pet_with_form**](docs/PetApi.md#update_pet_with_form) | **POST** /pet/{petId} | Updates a pet in the store with form data -*PetApi* | [**upload_file**](docs/PetApi.md#upload_file) | **POST** /pet/{petId}/uploadImage | uploads an image -*StoreApi* | [**delete_order**](docs/StoreApi.md#delete_order) | **DELETE** /store/order/{orderId} | Delete purchase order by ID -*StoreApi* | [**get_inventory**](docs/StoreApi.md#get_inventory) | **GET** /store/inventory | Returns pet inventories by status -*StoreApi* | [**get_order_by_id**](docs/StoreApi.md#get_order_by_id) | **GET** /store/order/{orderId} | Find purchase order by ID -*StoreApi* | [**place_order**](docs/StoreApi.md#place_order) | **POST** /store/order | Place an order for a pet -*UserApi* | [**create_user**](docs/UserApi.md#create_user) | **POST** /user | Create user -*UserApi* | [**create_users_with_array_input**](docs/UserApi.md#create_users_with_array_input) | **POST** /user/createWithArray | Creates list of users with given input array -*UserApi* | [**create_users_with_list_input**](docs/UserApi.md#create_users_with_list_input) | **POST** /user/createWithList | Creates list of users with given input array -*UserApi* | [**delete_user**](docs/UserApi.md#delete_user) | **DELETE** /user/{username} | Delete user -*UserApi* | [**get_user_by_name**](docs/UserApi.md#get_user_by_name) | **GET** /user/{username} | Get user by user name -*UserApi* | [**login_user**](docs/UserApi.md#login_user) | **GET** /user/login | Logs user into the system -*UserApi* | [**logout_user**](docs/UserApi.md#logout_user) | **GET** /user/logout | Logs out current logged in user session -*UserApi* | [**update_user**](docs/UserApi.md#update_user) | **PUT** /user/{username} | Updated user - - - -## Models - - - [ApiResponse](docs/ApiResponse.md) - - [Category](docs/Category.md) - - [Order](docs/Order.md) - - [Pet](docs/Pet.md) - - [Tag](docs/Tag.md) - - [User](docs/User.md) - - - -## Author - - - diff --git a/test/server/petstore_v3/petstore/docs/ApiResponse.md b/test/server/petstore_v3/petstore/docs/ApiResponse.md deleted file mode 100644 index 664dd24..0000000 --- a/test/server/petstore_v3/petstore/docs/ApiResponse.md +++ /dev/null @@ -1,14 +0,0 @@ -# ApiResponse - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**code** | **Int64** | | [optional] [default to nothing] -**type** | **String** | | [optional] [default to nothing] -**message** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/petstore_v3/petstore/docs/Category.md b/test/server/petstore_v3/petstore/docs/Category.md deleted file mode 100644 index 4e93290..0000000 --- a/test/server/petstore_v3/petstore/docs/Category.md +++ /dev/null @@ -1,13 +0,0 @@ -# Category - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**name** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/petstore_v3/petstore/docs/Order.md b/test/server/petstore_v3/petstore/docs/Order.md deleted file mode 100644 index b815c05..0000000 --- a/test/server/petstore_v3/petstore/docs/Order.md +++ /dev/null @@ -1,17 +0,0 @@ -# Order - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**petId** | **Int64** | | [optional] [default to nothing] -**quantity** | **Int64** | | [optional] [default to nothing] -**shipDate** | **ZonedDateTime** | | [optional] [default to nothing] -**status** | **String** | Order Status | [optional] [default to nothing] -**complete** | **Bool** | | [optional] [default to false] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/petstore_v3/petstore/docs/Pet.md b/test/server/petstore_v3/petstore/docs/Pet.md deleted file mode 100644 index a8bba4c..0000000 --- a/test/server/petstore_v3/petstore/docs/Pet.md +++ /dev/null @@ -1,17 +0,0 @@ -# Pet - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**category** | [***Category**](Category.md) | | [optional] [default to nothing] -**name** | **String** | | [default to nothing] -**photoUrls** | **Vector{String}** | | [default to nothing] -**tags** | [**Vector{Tag}**](Tag.md) | | [optional] [default to nothing] -**status** | **String** | pet status in the store | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/petstore_v3/petstore/docs/PetApi.md b/test/server/petstore_v3/petstore/docs/PetApi.md deleted file mode 100644 index 22e34a9..0000000 --- a/test/server/petstore_v3/petstore/docs/PetApi.md +++ /dev/null @@ -1,258 +0,0 @@ -# PetApi - -All URIs are relative to */v3* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**add_pet**](PetApi.md#add_pet) | **POST** /pet | Add a new pet to the store -[**delete_pet**](PetApi.md#delete_pet) | **DELETE** /pet/{petId} | Deletes a pet -[**find_pets_by_status**](PetApi.md#find_pets_by_status) | **GET** /pet/findByStatus | Finds Pets by status -[**find_pets_by_tags**](PetApi.md#find_pets_by_tags) | **GET** /pet/findByTags | Finds Pets by tags -[**get_pet_by_id**](PetApi.md#get_pet_by_id) | **GET** /pet/{petId} | Find pet by ID -[**update_pet**](PetApi.md#update_pet) | **PUT** /pet | Update an existing pet -[**update_pet_with_form**](PetApi.md#update_pet_with_form) | **POST** /pet/{petId} | Updates a pet in the store with form data -[**upload_file**](PetApi.md#upload_file) | **POST** /pet/{petId}/uploadImage | uploads an image - - -# **add_pet** -> add_pet(req::HTTP.Request, pet::Pet;) -> Nothing - -Add a new pet to the store - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**pet** | [**Pet**](Pet.md)| Pet object that needs to be added to the store | - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: application/json, application/xml - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **delete_pet** -> delete_pet(req::HTTP.Request, pet_id::Int64; api_key=nothing,) -> Nothing - -Deletes a pet - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**pet_id** | **Int64**| Pet id to delete | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **api_key** | **String**| | [default to nothing] - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **find_pets_by_status** -> find_pets_by_status(req::HTTP.Request, status::Vector{String};) -> Vector{Pet} - -Finds Pets by status - -Multiple status values can be provided with comma separated strings - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**status** | [**Vector{String}**](String.md)| Status values that need to be considered for filter | - -### Return type - -[**Vector{Pet}**](Pet.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **find_pets_by_tags** -> find_pets_by_tags(req::HTTP.Request, tags::Vector{String};) -> Vector{Pet} - -Finds Pets by tags - -Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**tags** | [**Vector{String}**](String.md)| Tags to filter by | - -### Return type - -[**Vector{Pet}**](Pet.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **get_pet_by_id** -> get_pet_by_id(req::HTTP.Request, pet_id::Int64;) -> Pet - -Find pet by ID - -Returns a single pet - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**pet_id** | **Int64**| ID of pet to return | - -### Return type - -[**Pet**](Pet.md) - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **update_pet** -> update_pet(req::HTTP.Request, pet::Pet;) -> Nothing - -Update an existing pet - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**pet** | [**Pet**](Pet.md)| Pet object that needs to be added to the store | - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: application/json, application/xml - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **update_pet_with_form** -> update_pet_with_form(req::HTTP.Request, pet_id::Int64; name=nothing, status=nothing,) -> Nothing - -Updates a pet in the store with form data - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**pet_id** | **Int64**| ID of pet that needs to be updated | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **name** | **String**| Updated name of the pet | [default to nothing] - **status** | **String**| Updated status of the pet | [default to nothing] - -### Return type - -Nothing - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: application/x-www-form-urlencoded - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **upload_file** -> upload_file(req::HTTP.Request, pet_id::Int64; additional_metadata=nothing, file=nothing,) -> ApiResponse - -uploads an image - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**pet_id** | **Int64**| ID of pet to update | - -### Optional Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **additional_metadata** | **String**| Additional data to pass to server | [default to nothing] - **file** | **Vector{UInt8}**| file to upload | - -### Return type - -[**ApiResponse**](ApiResponse.md) - -### Authorization - -[petstore_auth](../README.md#petstore_auth) - -### HTTP request headers - - - **Content-Type**: multipart/form-data - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/test/server/petstore_v3/petstore/docs/StoreApi.md b/test/server/petstore_v3/petstore/docs/StoreApi.md deleted file mode 100644 index 3bd28f9..0000000 --- a/test/server/petstore_v3/petstore/docs/StoreApi.md +++ /dev/null @@ -1,122 +0,0 @@ -# StoreApi - -All URIs are relative to */v3* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**delete_order**](StoreApi.md#delete_order) | **DELETE** /store/order/{orderId} | Delete purchase order by ID -[**get_inventory**](StoreApi.md#get_inventory) | **GET** /store/inventory | Returns pet inventories by status -[**get_order_by_id**](StoreApi.md#get_order_by_id) | **GET** /store/order/{orderId} | Find purchase order by ID -[**place_order**](StoreApi.md#place_order) | **POST** /store/order | Place an order for a pet - - -# **delete_order** -> delete_order(req::HTTP.Request, order_id::String;) -> Nothing - -Delete purchase order by ID - -For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**order_id** | **String**| ID of the order that needs to be deleted | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **get_inventory** -> get_inventory(req::HTTP.Request;) -> Dict{String, Int64} - -Returns pet inventories by status - -Returns a map of status codes to quantities - -### Required Parameters -This endpoint does not need any parameter. - -### Return type - -**Dict{String, Int64}** - -### Authorization - -[api_key](../README.md#api_key) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **get_order_by_id** -> get_order_by_id(req::HTTP.Request, order_id::Int64;) -> Order - -Find purchase order by ID - -For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**order_id** | **Int64**| ID of pet that needs to be fetched | - -### Return type - -[**Order**](Order.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **place_order** -> place_order(req::HTTP.Request, order::Order;) -> Order - -Place an order for a pet - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**order** | [**Order**](Order.md)| order placed for purchasing the pet | - -### Return type - -[**Order**](Order.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/test/server/petstore_v3/petstore/docs/Tag.md b/test/server/petstore_v3/petstore/docs/Tag.md deleted file mode 100644 index c904872..0000000 --- a/test/server/petstore_v3/petstore/docs/Tag.md +++ /dev/null @@ -1,13 +0,0 @@ -# Tag - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**name** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/petstore_v3/petstore/docs/User.md b/test/server/petstore_v3/petstore/docs/User.md deleted file mode 100644 index 5318b5a..0000000 --- a/test/server/petstore_v3/petstore/docs/User.md +++ /dev/null @@ -1,19 +0,0 @@ -# User - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**id** | **Int64** | | [optional] [default to nothing] -**username** | **String** | | [optional] [default to nothing] -**firstName** | **String** | | [optional] [default to nothing] -**lastName** | **String** | | [optional] [default to nothing] -**email** | **String** | | [optional] [default to nothing] -**password** | **String** | | [optional] [default to nothing] -**phone** | **String** | | [optional] [default to nothing] -**userStatus** | **Int64** | User Status | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/petstore_v3/petstore/docs/UserApi.md b/test/server/petstore_v3/petstore/docs/UserApi.md deleted file mode 100644 index e6b0821..0000000 --- a/test/server/petstore_v3/petstore/docs/UserApi.md +++ /dev/null @@ -1,236 +0,0 @@ -# UserApi - -All URIs are relative to */v3* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**create_user**](UserApi.md#create_user) | **POST** /user | Create user -[**create_users_with_array_input**](UserApi.md#create_users_with_array_input) | **POST** /user/createWithArray | Creates list of users with given input array -[**create_users_with_list_input**](UserApi.md#create_users_with_list_input) | **POST** /user/createWithList | Creates list of users with given input array -[**delete_user**](UserApi.md#delete_user) | **DELETE** /user/{username} | Delete user -[**get_user_by_name**](UserApi.md#get_user_by_name) | **GET** /user/{username} | Get user by user name -[**login_user**](UserApi.md#login_user) | **GET** /user/login | Logs user into the system -[**logout_user**](UserApi.md#logout_user) | **GET** /user/logout | Logs out current logged in user session -[**update_user**](UserApi.md#update_user) | **PUT** /user/{username} | Updated user - - -# **create_user** -> create_user(req::HTTP.Request, user::User;) -> Nothing - -Create user - -This can only be done by the logged in user. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**user** | [**User**](User.md)| Created user object | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **create_users_with_array_input** -> create_users_with_array_input(req::HTTP.Request, user::Vector{User};) -> Nothing - -Creates list of users with given input array - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**user** | [**Vector{User}**](User.md)| List of user object | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **create_users_with_list_input** -> create_users_with_list_input(req::HTTP.Request, user::Vector{User};) -> Nothing - -Creates list of users with given input array - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**user** | [**Vector{User}**](User.md)| List of user object | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **delete_user** -> delete_user(req::HTTP.Request, username::String;) -> Nothing - -Delete user - -This can only be done by the logged in user. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**username** | **String**| The name that needs to be deleted | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **get_user_by_name** -> get_user_by_name(req::HTTP.Request, username::String;) -> User - -Get user by user name - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**username** | **String**| The name that needs to be fetched. Use user1 for testing. | - -### Return type - -[**User**](User.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **login_user** -> login_user(req::HTTP.Request, username::String, password::String;) -> String - -Logs user into the system - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**username** | **String**| The user name for login | -**password** | **String**| The password for login in clear text | - -### Return type - -**String** - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/xml, application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **logout_user** -> logout_user(req::HTTP.Request;) -> Nothing - -Logs out current logged in user session - -### Required Parameters -This endpoint does not need any parameter. - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **update_user** -> update_user(req::HTTP.Request, username::String, user::User;) -> Nothing - -Updated user - -This can only be done by the logged in user. - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**username** | **String**| name that need to be deleted | -**user** | [**User**](User.md)| Updated user object | - -### Return type - -Nothing - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/test/server/petstore_v3/petstore/src/PetStoreServer.jl b/test/server/petstore_v3/petstore/src/PetStoreServer.jl deleted file mode 100644 index aa18501..0000000 --- a/test/server/petstore_v3/petstore/src/PetStoreServer.jl +++ /dev/null @@ -1,123 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw""" -Encapsulates generated server code for PetStoreServer - -The following server methods must be implemented: - -- **add_pet** - - *invocation:* POST /pet - - *signature:* add_pet(req::HTTP.Request, pet::Pet;) -> Nothing -- **delete_pet** - - *invocation:* DELETE /pet/{petId} - - *signature:* delete_pet(req::HTTP.Request, pet_id::Int64; api_key=nothing,) -> Nothing -- **find_pets_by_status** - - *invocation:* GET /pet/findByStatus - - *signature:* find_pets_by_status(req::HTTP.Request, status::Vector{String};) -> Vector{Pet} -- **find_pets_by_tags** - - *invocation:* GET /pet/findByTags - - *signature:* find_pets_by_tags(req::HTTP.Request, tags::Vector{String};) -> Vector{Pet} -- **get_pet_by_id** - - *invocation:* GET /pet/{petId} - - *signature:* get_pet_by_id(req::HTTP.Request, pet_id::Int64;) -> Pet -- **update_pet** - - *invocation:* PUT /pet - - *signature:* update_pet(req::HTTP.Request, pet::Pet;) -> Nothing -- **update_pet_with_form** - - *invocation:* POST /pet/{petId} - - *signature:* update_pet_with_form(req::HTTP.Request, pet_id::Int64; name=nothing, status=nothing,) -> Nothing -- **upload_file** - - *invocation:* POST /pet/{petId}/uploadImage - - *signature:* upload_file(req::HTTP.Request, pet_id::Int64; additional_metadata=nothing, file=nothing,) -> ApiResponse -- **delete_order** - - *invocation:* DELETE /store/order/{orderId} - - *signature:* delete_order(req::HTTP.Request, order_id::String;) -> Nothing -- **get_inventory** - - *invocation:* GET /store/inventory - - *signature:* get_inventory(req::HTTP.Request;) -> Dict{String, Int64} -- **get_order_by_id** - - *invocation:* GET /store/order/{orderId} - - *signature:* get_order_by_id(req::HTTP.Request, order_id::Int64;) -> Order -- **place_order** - - *invocation:* POST /store/order - - *signature:* place_order(req::HTTP.Request, order::Order;) -> Order -- **create_user** - - *invocation:* POST /user - - *signature:* create_user(req::HTTP.Request, user::User;) -> Nothing -- **create_users_with_array_input** - - *invocation:* POST /user/createWithArray - - *signature:* create_users_with_array_input(req::HTTP.Request, user::Vector{User};) -> Nothing -- **create_users_with_list_input** - - *invocation:* POST /user/createWithList - - *signature:* create_users_with_list_input(req::HTTP.Request, user::Vector{User};) -> Nothing -- **delete_user** - - *invocation:* DELETE /user/{username} - - *signature:* delete_user(req::HTTP.Request, username::String;) -> Nothing -- **get_user_by_name** - - *invocation:* GET /user/{username} - - *signature:* get_user_by_name(req::HTTP.Request, username::String;) -> User -- **login_user** - - *invocation:* GET /user/login - - *signature:* login_user(req::HTTP.Request, username::String, password::String;) -> String -- **logout_user** - - *invocation:* GET /user/logout - - *signature:* logout_user(req::HTTP.Request;) -> Nothing -- **update_user** - - *invocation:* PUT /user/{username} - - *signature:* update_user(req::HTTP.Request, username::String, user::User;) -> Nothing -""" -module PetStoreServer - -using HTTP -using URIs -using Dates -using TimeZones -using OpenAPI -using OpenAPI.Servers - -const API_VERSION = "1.0.0" - -include("modelincludes.jl") - -include("apis/api_PetApi.jl") -include("apis/api_StoreApi.jl") -include("apis/api_UserApi.jl") - -""" -Register handlers for all APIs in this module in the supplied `Router` instance. - -Paramerets: -- `router`: Router to register handlers in -- `impl`: module that implements the server methods - -Optional parameters: -- `path_prefix`: prefix to be applied to all paths -- `optional_middlewares`: Register one or more optional middlewares to be applied to all requests. - -Optional middlewares can be one or more of: - - `init`: called before the request is processed - - `pre_validation`: called after the request is parsed but before validation - - `pre_invoke`: called after validation but before the handler is invoked - - `post_invoke`: called after the handler is invoked but before the response is sent - -The order in which middlewares are invoked are: -`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` -""" -function register(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - registerPetApi(router, impl; path_prefix=path_prefix, optional_middlewares...) - registerStoreApi(router, impl; path_prefix=path_prefix, optional_middlewares...) - registerUserApi(router, impl; path_prefix=path_prefix, optional_middlewares...) - return router -end - -# export models -export ApiResponse -export Category -export Order -export Pet -export Tag -export User - -end # module PetStoreServer diff --git a/test/server/petstore_v3/petstore/src/apis/api_PetApi.jl b/test/server/petstore_v3/petstore/src/apis/api_PetApi.jl deleted file mode 100644 index f3e3346..0000000 --- a/test/server/petstore_v3/petstore/src/apis/api_PetApi.jl +++ /dev/null @@ -1,407 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -function add_pet_read(handler) - function add_pet_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["Pet"] = OpenAPI.Servers.to_param_type(Pet, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function add_pet_validate(handler) - function add_pet_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "add_pet" - - n = "Pet" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function add_pet_invoke(impl; post_invoke=nothing) - function add_pet_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.add_pet(req::HTTP.Request, openapi_params["Pet"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function delete_pet_read(handler) - function delete_pet_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["petId"] = OpenAPI.Servers.to_param(Int64, path_params, "petId", required=true, ) - headers = Dict{String,String}(req.headers) - openapi_params["api_key"] = OpenAPI.Servers.to_param(String, headers, "api_key", ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function delete_pet_validate(handler) - function delete_pet_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "delete_pet" - - n = "petId" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "api_key" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function delete_pet_invoke(impl; post_invoke=nothing) - function delete_pet_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.delete_pet(req::HTTP.Request, openapi_params["petId"]; api_key=get(openapi_params, "api_key", nothing),) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function find_pets_by_status_read(handler) - function find_pets_by_status_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - query_params = HTTP.queryparams(URIs.URI(req.target)) - openapi_params["status"] = OpenAPI.Servers.to_param(Vector{String}, query_params, "status", required=true, style="form", is_explode=false) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function find_pets_by_status_validate(handler) - function find_pets_by_status_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "find_pets_by_status" - - n = "status" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function find_pets_by_status_invoke(impl; post_invoke=nothing) - function find_pets_by_status_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.find_pets_by_status(req::HTTP.Request, openapi_params["status"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function find_pets_by_tags_read(handler) - function find_pets_by_tags_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - query_params = HTTP.queryparams(URIs.URI(req.target)) - openapi_params["tags"] = OpenAPI.Servers.to_param(Vector{String}, query_params, "tags", required=true, style="form", is_explode=false) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function find_pets_by_tags_validate(handler) - function find_pets_by_tags_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "find_pets_by_tags" - - n = "tags" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function find_pets_by_tags_invoke(impl; post_invoke=nothing) - function find_pets_by_tags_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.find_pets_by_tags(req::HTTP.Request, openapi_params["tags"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_pet_by_id_read(handler) - function get_pet_by_id_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["petId"] = OpenAPI.Servers.to_param(Int64, path_params, "petId", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_pet_by_id_validate(handler) - function get_pet_by_id_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "get_pet_by_id" - - n = "petId" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function get_pet_by_id_invoke(impl; post_invoke=nothing) - function get_pet_by_id_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_pet_by_id(req::HTTP.Request, openapi_params["petId"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function update_pet_read(handler) - function update_pet_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["Pet"] = OpenAPI.Servers.to_param_type(Pet, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function update_pet_validate(handler) - function update_pet_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "update_pet" - - n = "Pet" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function update_pet_invoke(impl; post_invoke=nothing) - function update_pet_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.update_pet(req::HTTP.Request, openapi_params["Pet"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function update_pet_with_form_read(handler) - function update_pet_with_form_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["petId"] = OpenAPI.Servers.to_param(Int64, path_params, "petId", required=true, ) - ismultipart = false - form_data = ismultipart ? HTTP.parse_multipart_form(req) : HTTP.queryparams(String(copy(req.body))) - openapi_params["name"] = OpenAPI.Servers.to_param(String, form_data, "name"; multipart=ismultipart, isfile=false, ) - openapi_params["status"] = OpenAPI.Servers.to_param(String, form_data, "status"; multipart=ismultipart, isfile=false, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function update_pet_with_form_validate(handler) - function update_pet_with_form_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "update_pet_with_form" - - n = "petId" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "name" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "status" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function update_pet_with_form_invoke(impl; post_invoke=nothing) - function update_pet_with_form_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.update_pet_with_form(req::HTTP.Request, openapi_params["petId"]; name=get(openapi_params, "name", nothing), status=get(openapi_params, "status", nothing),) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function upload_file_read(handler) - function upload_file_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["petId"] = OpenAPI.Servers.to_param(Int64, path_params, "petId", required=true, ) - ismultipart = true - form_data = ismultipart ? HTTP.parse_multipart_form(req) : HTTP.queryparams(String(copy(req.body))) - openapi_params["additionalMetadata"] = OpenAPI.Servers.to_param(String, form_data, "additionalMetadata"; multipart=ismultipart, isfile=false, ) - openapi_params["file"] = OpenAPI.Servers.to_param(Vector{UInt8}, form_data, "file"; multipart=ismultipart, isfile=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function upload_file_validate(handler) - function upload_file_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "upload_file" - - n = "petId" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "additionalMetadata" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "file" - v = get(openapi_params, n, nothing) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function upload_file_invoke(impl; post_invoke=nothing) - function upload_file_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.upload_file(req::HTTP.Request, openapi_params["petId"]; additional_metadata=get(openapi_params, "additionalMetadata", nothing), file=get(openapi_params, "file", nothing),) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - - -function registerPetApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - HTTP.register!(router, "POST", path_prefix * "/pet", OpenAPI.Servers.middleware(impl, add_pet_read, add_pet_validate, add_pet_invoke; optional_middlewares...)) - HTTP.register!(router, "DELETE", path_prefix * "/pet/{petId}", OpenAPI.Servers.middleware(impl, delete_pet_read, delete_pet_validate, delete_pet_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/pet/findByStatus", OpenAPI.Servers.middleware(impl, find_pets_by_status_read, find_pets_by_status_validate, find_pets_by_status_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/pet/findByTags", OpenAPI.Servers.middleware(impl, find_pets_by_tags_read, find_pets_by_tags_validate, find_pets_by_tags_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/pet/{petId}", OpenAPI.Servers.middleware(impl, get_pet_by_id_read, get_pet_by_id_validate, get_pet_by_id_invoke; optional_middlewares...)) - HTTP.register!(router, "PUT", path_prefix * "/pet", OpenAPI.Servers.middleware(impl, update_pet_read, update_pet_validate, update_pet_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/pet/{petId}", OpenAPI.Servers.middleware(impl, update_pet_with_form_read, update_pet_with_form_validate, update_pet_with_form_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/pet/{petId}/uploadImage", OpenAPI.Servers.middleware(impl, upload_file_read, upload_file_validate, upload_file_invoke; optional_middlewares...)) - return router -end diff --git a/test/server/petstore_v3/petstore/src/apis/api_StoreApi.jl b/test/server/petstore_v3/petstore/src/apis/api_StoreApi.jl deleted file mode 100644 index 4a8b69f..0000000 --- a/test/server/petstore_v3/petstore/src/apis/api_StoreApi.jl +++ /dev/null @@ -1,157 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -function delete_order_read(handler) - function delete_order_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["orderId"] = OpenAPI.Servers.to_param(String, path_params, "orderId", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function delete_order_validate(handler) - function delete_order_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "delete_order" - - n = "orderId" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function delete_order_invoke(impl; post_invoke=nothing) - function delete_order_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.delete_order(req::HTTP.Request, openapi_params["orderId"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_inventory_read(handler) - function get_inventory_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_inventory_validate(handler) - function get_inventory_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "get_inventory" - - return handler(req) - end -end - -function get_inventory_invoke(impl; post_invoke=nothing) - function get_inventory_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_inventory(req::HTTP.Request;) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_order_by_id_read(handler) - function get_order_by_id_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["orderId"] = OpenAPI.Servers.to_param(Int64, path_params, "orderId", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_order_by_id_validate(handler) - function get_order_by_id_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "get_order_by_id" - - n = "orderId" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - OpenAPI.validate_param(n, op, :maximum, v, 5, false) - OpenAPI.validate_param(n, op, :minimum, v, 1, false) - end - - return handler(req) - end -end - -function get_order_by_id_invoke(impl; post_invoke=nothing) - function get_order_by_id_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_order_by_id(req::HTTP.Request, openapi_params["orderId"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function place_order_read(handler) - function place_order_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["Order"] = OpenAPI.Servers.to_param_type(Order, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function place_order_validate(handler) - function place_order_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "place_order" - - n = "Order" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function place_order_invoke(impl; post_invoke=nothing) - function place_order_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.place_order(req::HTTP.Request, openapi_params["Order"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - - -function registerStoreApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - HTTP.register!(router, "DELETE", path_prefix * "/store/order/{orderId}", OpenAPI.Servers.middleware(impl, delete_order_read, delete_order_validate, delete_order_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/store/inventory", OpenAPI.Servers.middleware(impl, get_inventory_read, get_inventory_validate, get_inventory_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/store/order/{orderId}", OpenAPI.Servers.middleware(impl, get_order_by_id_read, get_order_by_id_validate, get_order_by_id_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/store/order", OpenAPI.Servers.middleware(impl, place_order_read, place_order_validate, place_order_invoke; optional_middlewares...)) - return router -end diff --git a/test/server/petstore_v3/petstore/src/apis/api_UserApi.jl b/test/server/petstore_v3/petstore/src/apis/api_UserApi.jl deleted file mode 100644 index fc226f0..0000000 --- a/test/server/petstore_v3/petstore/src/apis/api_UserApi.jl +++ /dev/null @@ -1,353 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -function create_user_read(handler) - function create_user_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["User"] = OpenAPI.Servers.to_param_type(User, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function create_user_validate(handler) - function create_user_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "create_user" - - n = "User" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function create_user_invoke(impl; post_invoke=nothing) - function create_user_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.create_user(req::HTTP.Request, openapi_params["User"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function create_users_with_array_input_read(handler) - function create_users_with_array_input_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["User"] = OpenAPI.Servers.to_param_type(Vector{User}, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function create_users_with_array_input_validate(handler) - function create_users_with_array_input_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "create_users_with_array_input" - - n = "User" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function create_users_with_array_input_invoke(impl; post_invoke=nothing) - function create_users_with_array_input_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.create_users_with_array_input(req::HTTP.Request, openapi_params["User"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function create_users_with_list_input_read(handler) - function create_users_with_list_input_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["User"] = OpenAPI.Servers.to_param_type(Vector{User}, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function create_users_with_list_input_validate(handler) - function create_users_with_list_input_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "create_users_with_list_input" - - n = "User" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function create_users_with_list_input_invoke(impl; post_invoke=nothing) - function create_users_with_list_input_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.create_users_with_list_input(req::HTTP.Request, openapi_params["User"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function delete_user_read(handler) - function delete_user_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["username"] = OpenAPI.Servers.to_param(String, path_params, "username", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function delete_user_validate(handler) - function delete_user_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "delete_user" - - n = "username" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function delete_user_invoke(impl; post_invoke=nothing) - function delete_user_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.delete_user(req::HTTP.Request, openapi_params["username"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_user_by_name_read(handler) - function get_user_by_name_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["username"] = OpenAPI.Servers.to_param(String, path_params, "username", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_user_by_name_validate(handler) - function get_user_by_name_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "get_user_by_name" - - n = "username" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function get_user_by_name_invoke(impl; post_invoke=nothing) - function get_user_by_name_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_user_by_name(req::HTTP.Request, openapi_params["username"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function login_user_read(handler) - function login_user_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - query_params = HTTP.queryparams(URIs.URI(req.target)) - openapi_params["username"] = OpenAPI.Servers.to_param(String, query_params, "username", required=true, style="form", is_explode=true) - openapi_params["password"] = OpenAPI.Servers.to_param(String, query_params, "password", required=true, style="form", is_explode=true) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function login_user_validate(handler) - function login_user_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "login_user" - - n = "username" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "password" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function login_user_invoke(impl; post_invoke=nothing) - function login_user_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.login_user(req::HTTP.Request, openapi_params["username"], openapi_params["password"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function logout_user_read(handler) - function logout_user_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function logout_user_validate(handler) - function logout_user_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "logout_user" - - return handler(req) - end -end - -function logout_user_invoke(impl; post_invoke=nothing) - function logout_user_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.logout_user(req::HTTP.Request;) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function update_user_read(handler) - function update_user_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["username"] = OpenAPI.Servers.to_param(String, path_params, "username", required=true, ) - openapi_params["User"] = OpenAPI.Servers.to_param_type(User, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function update_user_validate(handler) - function update_user_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "update_user" - - n = "username" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - n = "User" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - if isa(v, OpenAPI.APIModel) - OpenAPI.validate_properties(v) - if !OpenAPI.check_required(v) - throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) - end - end - end - - return handler(req) - end -end - -function update_user_invoke(impl; post_invoke=nothing) - function update_user_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.update_user(req::HTTP.Request, openapi_params["username"], openapi_params["User"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - - -function registerUserApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - HTTP.register!(router, "POST", path_prefix * "/user", OpenAPI.Servers.middleware(impl, create_user_read, create_user_validate, create_user_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/user/createWithArray", OpenAPI.Servers.middleware(impl, create_users_with_array_input_read, create_users_with_array_input_validate, create_users_with_array_input_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/user/createWithList", OpenAPI.Servers.middleware(impl, create_users_with_list_input_read, create_users_with_list_input_validate, create_users_with_list_input_invoke; optional_middlewares...)) - HTTP.register!(router, "DELETE", path_prefix * "/user/{username}", OpenAPI.Servers.middleware(impl, delete_user_read, delete_user_validate, delete_user_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/user/{username}", OpenAPI.Servers.middleware(impl, get_user_by_name_read, get_user_by_name_validate, get_user_by_name_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/user/login", OpenAPI.Servers.middleware(impl, login_user_read, login_user_validate, login_user_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/user/logout", OpenAPI.Servers.middleware(impl, logout_user_read, logout_user_validate, logout_user_invoke; optional_middlewares...)) - HTTP.register!(router, "PUT", path_prefix * "/user/{username}", OpenAPI.Servers.middleware(impl, update_user_read, update_user_validate, update_user_invoke; optional_middlewares...)) - return router -end diff --git a/test/server/petstore_v3/petstore/src/modelincludes.jl b/test/server/petstore_v3/petstore/src/modelincludes.jl deleted file mode 100644 index b3a3db8..0000000 --- a/test/server/petstore_v3/petstore/src/modelincludes.jl +++ /dev/null @@ -1,9 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -include("models/model_ApiResponse.jl") -include("models/model_Category.jl") -include("models/model_Order.jl") -include("models/model_Pet.jl") -include("models/model_Tag.jl") -include("models/model_User.jl") diff --git a/test/server/petstore_v3/petstore/src/models/model_ApiResponse.jl b/test/server/petstore_v3/petstore/src/models/model_ApiResponse.jl deleted file mode 100644 index 1d6b413..0000000 --- a/test/server/petstore_v3/petstore/src/models/model_ApiResponse.jl +++ /dev/null @@ -1,50 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""ApiResponse -Describes the result of uploading an image resource - - ApiResponse(; - code=nothing, - type=nothing, - message=nothing, - ) - - - code::Int64 - - type::String - - message::String -""" -Base.@kwdef mutable struct ApiResponse <: OpenAPI.APIModel - code::Union{Nothing, Int64} = nothing - type::Union{Nothing, String} = nothing - message::Union{Nothing, String} = nothing - - function ApiResponse(code, type, message, ) - o = new(code, type, message, ) - OpenAPI.validate_properties(o) - return o - end -end # type ApiResponse - -const _property_types_ApiResponse = Dict{Symbol,String}(Symbol("code")=>"Int64", Symbol("type")=>"String", Symbol("message")=>"String", ) -OpenAPI.property_type(::Type{ ApiResponse }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_ApiResponse[name]))} - -function OpenAPI.check_required(o::ApiResponse) - true -end - -function OpenAPI.validate_properties(o::ApiResponse) - OpenAPI.validate_property(ApiResponse, Symbol("code"), o.code) - OpenAPI.validate_property(ApiResponse, Symbol("type"), o.type) - OpenAPI.validate_property(ApiResponse, Symbol("message"), o.message) -end - -function OpenAPI.validate_property(::Type{ ApiResponse }, name::Symbol, val) - - if name === Symbol("code") - OpenAPI.validate_param(name, "ApiResponse", :format, val, "int32") - end - - -end diff --git a/test/server/petstore_v3/petstore/src/models/model_Category.jl b/test/server/petstore_v3/petstore/src/models/model_Category.jl deleted file mode 100644 index 843d8a5..0000000 --- a/test/server/petstore_v3/petstore/src/models/model_Category.jl +++ /dev/null @@ -1,45 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Category -A category for a pet - - Category(; - id=nothing, - name=nothing, - ) - - - id::Int64 - - name::String -""" -Base.@kwdef mutable struct Category <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - name::Union{Nothing, String} = nothing - - function Category(id, name, ) - o = new(id, name, ) - OpenAPI.validate_properties(o) - return o - end -end # type Category - -const _property_types_Category = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("name")=>"String", ) -OpenAPI.property_type(::Type{ Category }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Category[name]))} - -function OpenAPI.check_required(o::Category) - true -end - -function OpenAPI.validate_properties(o::Category) - OpenAPI.validate_property(Category, Symbol("id"), o.id) - OpenAPI.validate_property(Category, Symbol("name"), o.name) -end - -function OpenAPI.validate_property(::Type{ Category }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "Category", :format, val, "int64") - end - -end diff --git a/test/server/petstore_v3/petstore/src/models/model_Order.jl b/test/server/petstore_v3/petstore/src/models/model_Order.jl deleted file mode 100644 index c1e95bc..0000000 --- a/test/server/petstore_v3/petstore/src/models/model_Order.jl +++ /dev/null @@ -1,78 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Order -An order for a pets from the pet store - - Order(; - id=nothing, - petId=nothing, - quantity=nothing, - shipDate=nothing, - status=nothing, - complete=false, - ) - - - id::Int64 - - petId::Int64 - - quantity::Int64 - - shipDate::ZonedDateTime - - status::String : Order Status - - complete::Bool -""" -Base.@kwdef mutable struct Order <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - petId::Union{Nothing, Int64} = nothing - quantity::Union{Nothing, Int64} = nothing - shipDate::Union{Nothing, ZonedDateTime} = nothing - status::Union{Nothing, String} = nothing - complete::Union{Nothing, Bool} = false - - function Order(id, petId, quantity, shipDate, status, complete, ) - o = new(id, petId, quantity, shipDate, status, complete, ) - OpenAPI.validate_properties(o) - return o - end -end # type Order - -const _property_types_Order = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("petId")=>"Int64", Symbol("quantity")=>"Int64", Symbol("shipDate")=>"ZonedDateTime", Symbol("status")=>"String", Symbol("complete")=>"Bool", ) -OpenAPI.property_type(::Type{ Order }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Order[name]))} - -function OpenAPI.check_required(o::Order) - true -end - -function OpenAPI.validate_properties(o::Order) - OpenAPI.validate_property(Order, Symbol("id"), o.id) - OpenAPI.validate_property(Order, Symbol("petId"), o.petId) - OpenAPI.validate_property(Order, Symbol("quantity"), o.quantity) - OpenAPI.validate_property(Order, Symbol("shipDate"), o.shipDate) - OpenAPI.validate_property(Order, Symbol("status"), o.status) - OpenAPI.validate_property(Order, Symbol("complete"), o.complete) -end - -function OpenAPI.validate_property(::Type{ Order }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "Order", :format, val, "int64") - end - - if name === Symbol("petId") - OpenAPI.validate_param(name, "Order", :format, val, "int64") - end - - if name === Symbol("quantity") - OpenAPI.validate_param(name, "Order", :format, val, "int32") - end - - if name === Symbol("shipDate") - OpenAPI.validate_param(name, "Order", :format, val, "date-time") - end - - if name === Symbol("status") - OpenAPI.validate_param(name, "Order", :enum, val, ["placed", "approved", "delivered"]) - end - - -end diff --git a/test/server/petstore_v3/petstore/src/models/model_Pet.jl b/test/server/petstore_v3/petstore/src/models/model_Pet.jl deleted file mode 100644 index 47f8f83..0000000 --- a/test/server/petstore_v3/petstore/src/models/model_Pet.jl +++ /dev/null @@ -1,71 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Pet -A pet for sale in the pet store - - Pet(; - id=nothing, - category=nothing, - name=nothing, - photoUrls=nothing, - tags=nothing, - status=nothing, - ) - - - id::Int64 - - category::Category - - name::String - - photoUrls::Vector{String} - - tags::Vector{Tag} - - status::String : pet status in the store -""" -Base.@kwdef mutable struct Pet <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - category = nothing # spec type: Union{ Nothing, Category } - name::Union{Nothing, String} = nothing - photoUrls::Union{Nothing, Vector{String}} = nothing - tags::Union{Nothing, Vector} = nothing # spec type: Union{ Nothing, Vector{Tag} } - status::Union{Nothing, String} = nothing - - function Pet(id, category, name, photoUrls, tags, status, ) - o = new(id, category, name, photoUrls, tags, status, ) - OpenAPI.validate_properties(o) - return o - end -end # type Pet - -const _property_types_Pet = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("category")=>"Category", Symbol("name")=>"String", Symbol("photoUrls")=>"Vector{String}", Symbol("tags")=>"Vector{Tag}", Symbol("status")=>"String", ) -OpenAPI.property_type(::Type{ Pet }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Pet[name]))} - -function OpenAPI.check_required(o::Pet) - o.name === nothing && (return false) - o.photoUrls === nothing && (return false) - true -end - -function OpenAPI.validate_properties(o::Pet) - OpenAPI.validate_property(Pet, Symbol("id"), o.id) - OpenAPI.validate_property(Pet, Symbol("category"), o.category) - OpenAPI.validate_property(Pet, Symbol("name"), o.name) - OpenAPI.validate_property(Pet, Symbol("photoUrls"), o.photoUrls) - OpenAPI.validate_property(Pet, Symbol("tags"), o.tags) - OpenAPI.validate_property(Pet, Symbol("status"), o.status) -end - -function OpenAPI.validate_property(::Type{ Pet }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "Pet", :format, val, "int64") - end - - - - - - if name === Symbol("status") - OpenAPI.validate_param(name, "Pet", :enum, val, ["available", "pending", "sold"]) - end - -end diff --git a/test/server/petstore_v3/petstore/src/models/model_Tag.jl b/test/server/petstore_v3/petstore/src/models/model_Tag.jl deleted file mode 100644 index 2743b59..0000000 --- a/test/server/petstore_v3/petstore/src/models/model_Tag.jl +++ /dev/null @@ -1,45 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""Tag -A tag for a pet - - Tag(; - id=nothing, - name=nothing, - ) - - - id::Int64 - - name::String -""" -Base.@kwdef mutable struct Tag <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - name::Union{Nothing, String} = nothing - - function Tag(id, name, ) - o = new(id, name, ) - OpenAPI.validate_properties(o) - return o - end -end # type Tag - -const _property_types_Tag = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("name")=>"String", ) -OpenAPI.property_type(::Type{ Tag }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_Tag[name]))} - -function OpenAPI.check_required(o::Tag) - true -end - -function OpenAPI.validate_properties(o::Tag) - OpenAPI.validate_property(Tag, Symbol("id"), o.id) - OpenAPI.validate_property(Tag, Symbol("name"), o.name) -end - -function OpenAPI.validate_property(::Type{ Tag }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "Tag", :format, val, "int64") - end - -end diff --git a/test/server/petstore_v3/petstore/src/models/model_User.jl b/test/server/petstore_v3/petstore/src/models/model_User.jl deleted file mode 100644 index 8e95c9a..0000000 --- a/test/server/petstore_v3/petstore/src/models/model_User.jl +++ /dev/null @@ -1,78 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""User -A User who is purchasing from the pet store - - User(; - id=nothing, - username=nothing, - firstName=nothing, - lastName=nothing, - email=nothing, - password=nothing, - phone=nothing, - userStatus=nothing, - ) - - - id::Int64 - - username::String - - firstName::String - - lastName::String - - email::String - - password::String - - phone::String - - userStatus::Int64 : User Status -""" -Base.@kwdef mutable struct User <: OpenAPI.APIModel - id::Union{Nothing, Int64} = nothing - username::Union{Nothing, String} = nothing - firstName::Union{Nothing, String} = nothing - lastName::Union{Nothing, String} = nothing - email::Union{Nothing, String} = nothing - password::Union{Nothing, String} = nothing - phone::Union{Nothing, String} = nothing - userStatus::Union{Nothing, Int64} = nothing - - function User(id, username, firstName, lastName, email, password, phone, userStatus, ) - o = new(id, username, firstName, lastName, email, password, phone, userStatus, ) - OpenAPI.validate_properties(o) - return o - end -end # type User - -const _property_types_User = Dict{Symbol,String}(Symbol("id")=>"Int64", Symbol("username")=>"String", Symbol("firstName")=>"String", Symbol("lastName")=>"String", Symbol("email")=>"String", Symbol("password")=>"String", Symbol("phone")=>"String", Symbol("userStatus")=>"Int64", ) -OpenAPI.property_type(::Type{ User }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_User[name]))} - -function OpenAPI.check_required(o::User) - true -end - -function OpenAPI.validate_properties(o::User) - OpenAPI.validate_property(User, Symbol("id"), o.id) - OpenAPI.validate_property(User, Symbol("username"), o.username) - OpenAPI.validate_property(User, Symbol("firstName"), o.firstName) - OpenAPI.validate_property(User, Symbol("lastName"), o.lastName) - OpenAPI.validate_property(User, Symbol("email"), o.email) - OpenAPI.validate_property(User, Symbol("password"), o.password) - OpenAPI.validate_property(User, Symbol("phone"), o.phone) - OpenAPI.validate_property(User, Symbol("userStatus"), o.userStatus) -end - -function OpenAPI.validate_property(::Type{ User }, name::Symbol, val) - - if name === Symbol("id") - OpenAPI.validate_param(name, "User", :format, val, "int64") - end - - - - - - - - if name === Symbol("userStatus") - OpenAPI.validate_param(name, "User", :format, val, "int32") - end -end diff --git a/test/server/petstore_v3/petstore_server.jl b/test/server/petstore_v3/petstore_server.jl deleted file mode 100644 index 0a8fb46..0000000 --- a/test/server/petstore_v3/petstore_server.jl +++ /dev/null @@ -1,169 +0,0 @@ -module PetStoreV3Server - -using HTTP - -include("petstore/src/PetStoreServer.jl") - -using .PetStoreServer - -const server = Ref{Any}(nothing) -const pets = Vector{Pet}() -const orders = Vector{Order}() -const users = Vector{User}() -const PRESET_TEST_USER = "user1" - -function add_pet(req::HTTP.Request, pet::Pet;) - push!(pets, pet) - return nothing -end - -function delete_pet(req::HTTP.Request, pet_id::Int64; api_key=nothing,) - filter!(x->x.id != pet_id, pets) - return nothing -end - -function find_pets_by_status(req::HTTP.Request, status::Vector{String};) - return filter(x->x.status == status, pets) -end - -function find_pets_by_tags(req::HTTP.Request, tags::Vector{String};) - return filter(x->!isempty(intersect(Set(x.tags), Set(tags))), pets) -end - -function get_pet_by_id(req::HTTP.Request, pet_id::Int64;) - pet = findfirst(x->x.id == pet_id, pets) - if pet === nothing - return HTTP.Response(404, "Pet not found") - else - return pets[pet] - end -end - -function update_pet(req::HTTP.Request, pet::Pet;) - filter!(x->x.id != pet.id, pets) - push!(pets, pet) - return nothing -end - -function update_pet_with_form(req::HTTP.Request, pet_id::Int64; name=nothing, status=nothing,) - for pet in pets - if pet.id == pet_id - if !isnothing(name) - pet.name = name - end - if !isnothing(status) - pet.status = status - end - end - end - return nothing -end - -function upload_file(req::HTTP.Request, pet_id::Int64; additional_metadata=nothing, file=nothing,) - return ApiResponse(; code=1, type="pet", message="file uploaded", ) -end - -function delete_order(req::HTTP.Request, order_id::String;) - filter!(x->x.id != order_id, orders) - return nothing -end - -function get_inventory(req::HTTP.Request;) - return Dict{String, Int64}( - "additionalProp1" => 0, - "additionalProp2" => 0, - "additionalProp3" => 0, - ) -end - -function get_order_by_id(req::HTTP.Request, order_id::Int64;) - order = findfirst(x->x.id == order_id, orders) - if order === nothing - return HTTP.Response(404, "Order not found") - else - return orders[order] - end -end - -function place_order(req::HTTP.Request, order::Order;) - if isnothing(order.id) - max_OrderId = isempty(orders) ? 0 : maximum(x->x.id, orders) - order.id = max_OrderId + 1 - end - push!(orders, order) - return order -end - -function create_user(req::HTTP.Request, user::User;) - push!(users, user) - return nothing -end - -function create_users_with_array_input(req::HTTP.Request, user::Vector{User};) - append!(users, user) - return nothing -end - -function create_users_with_list_input(req::HTTP.Request, user::Vector{User};) - append!(users, user) - return nothing -end - -function delete_user(req::HTTP.Request, username::String;) - filter!(x->x.username != username, users) - return nothing -end - -function get_user_by_name(req::HTTP.Request, username::String;) - # user = findfirst(x->x.username == username, users) - # if user === nothing - # return HTTP.Response(404, "User not found") - # else - # return user - # end - if username == PRESET_TEST_USER - return User(; id=1, username=PRESET_TEST_USER, firstName="John", lastName="Doe", email="jondoe@test.com", phone="1234567890", userStatus=1, ) - else - return HTTP.Response(404, "User not found") - end -end - -function login_user(req::HTTP.Request, username::String, password::String;) - return "logged in user session: test" -end - -function logout_user(req::HTTP.Request;) - return nothing -end - -function update_user(req::HTTP.Request, username::String, user::User;) - filter!(x->x.username != username, users) - push!(users, user) - return nothing -end - -function stop(::HTTP.Request) - HTTP.close(server[]) - return HTTP.Response(200, "") -end - -function ping(::HTTP.Request) - return HTTP.Response(200, "") -end - -function run_server(port=8081) - try - router = HTTP.Router() - router = PetStoreServer.register(router, @__MODULE__; path_prefix="/v3") - HTTP.register!(router, "GET", "/stop", stop) - HTTP.register!(router, "GET", "/ping", ping) - server[] = HTTP.serve!(router, port) - wait(server[]) - catch ex - @error("Server error", exception=(ex, catch_backtrace())) - end -end - -end # module PetStoreV3Server - -PetStoreV3Server.run_server() \ No newline at end of file diff --git a/test/server/timeouttest/TimeoutTestServer/.openapi-generator-ignore b/test/server/timeouttest/TimeoutTestServer/.openapi-generator-ignore deleted file mode 100644 index 7484ee5..0000000 --- a/test/server/timeouttest/TimeoutTestServer/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/test/server/timeouttest/TimeoutTestServer/.openapi-generator/FILES b/test/server/timeouttest/TimeoutTestServer/.openapi-generator/FILES deleted file mode 100644 index 5868e7c..0000000 --- a/test/server/timeouttest/TimeoutTestServer/.openapi-generator/FILES +++ /dev/null @@ -1,7 +0,0 @@ -README.md -docs/DefaultApi.md -docs/DelayresponseGet200Response.md -src/TimeoutTestServer.jl -src/apis/api_DefaultApi.jl -src/modelincludes.jl -src/models/model_DelayresponseGet200Response.jl diff --git a/test/server/timeouttest/TimeoutTestServer/.openapi-generator/VERSION b/test/server/timeouttest/TimeoutTestServer/.openapi-generator/VERSION deleted file mode 100644 index 4c631cf..0000000 --- a/test/server/timeouttest/TimeoutTestServer/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.14.0-SNAPSHOT diff --git a/test/server/timeouttest/TimeoutTestServer/README.md b/test/server/timeouttest/TimeoutTestServer/README.md deleted file mode 100644 index 8acab4c..0000000 --- a/test/server/timeouttest/TimeoutTestServer/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# Julia API server for TimeoutTestServer - -No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - -## Overview -This API server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. - -- API version: 1.0.0 -- Generator version: 7.14.0-SNAPSHOT -- Build package: org.openapitools.codegen.languages.JuliaServerCodegen - - -## Installation -Place the Julia files generated under the `src` folder in your Julia project. Include TimeoutTestServer.jl in the project code. -It would include the module named TimeoutTestServer. - -Implement the server methods as listed below. They are also documented with the TimeoutTestServer module. -Launch a HTTP server with a router that has all handlers registered. A `register` method is provided in TimeoutTestServer module for convenience. - -```julia -register( - router::HTTP.Router, # Router to register handlers in - impl; # Module that implements the server methods - path_prefix::String="", # Prefix to be applied to all paths - optional_middlewares... # Optional middlewares to be applied to all handlers -) -``` - -Optional middlewares can be one or more of: -- `init`: called before the request is processed -- `pre_validation`: called after the request is parsed but before validation -- `pre_invoke`: called after validation but before the handler is invoked -- `post_invoke`: called after the handler is invoked but before the response is sent - -The order in which middlewares are invoked are: -`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` - - -## API Endpoints - -The following server methods must be implemented: - -Class | Method | HTTP request | Description ------------- | ------------- | ------------- | ------------- -*DefaultApi* | [**delayresponse_get**](docs/DefaultApi.md#delayresponse_get) | **GET** /delayresponse | Delay Response Endpoint -*DefaultApi* | [**longpollstream_get**](docs/DefaultApi.md#longpollstream_get) | **GET** /longpollstream | Long polled streaming endpoint - - - -## Models - - - [DelayresponseGet200Response](docs/DelayresponseGet200Response.md) - - - -## Author - - - diff --git a/test/server/timeouttest/TimeoutTestServer/docs/DefaultApi.md b/test/server/timeouttest/TimeoutTestServer/docs/DefaultApi.md deleted file mode 100644 index b115a02..0000000 --- a/test/server/timeouttest/TimeoutTestServer/docs/DefaultApi.md +++ /dev/null @@ -1,64 +0,0 @@ -# DefaultApi - -All URIs are relative to *http://localhost* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**delayresponse_get**](DefaultApi.md#delayresponse_get) | **GET** /delayresponse | Delay Response Endpoint -[**longpollstream_get**](DefaultApi.md#longpollstream_get) | **GET** /longpollstream | Long polled streaming endpoint - - -# **delayresponse_get** -> delayresponse_get(req::HTTP.Request, delay_seconds::Int64;) -> DelayresponseGet200Response - -Delay Response Endpoint - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**delay_seconds** | **Int64**| Number of seconds to delay the response | - -### Return type - -[**DelayresponseGet200Response**](DelayresponseGet200Response.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **longpollstream_get** -> longpollstream_get(req::HTTP.Request, delay_seconds::Int64;) -> DelayresponseGet200Response - -Long polled streaming endpoint - -### Required Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **req** | **HTTP.Request** | The HTTP Request object | -**delay_seconds** | **Int64**| Number of seconds to delay the response | - -### Return type - -[**DelayresponseGet200Response**](DelayresponseGet200Response.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/test/server/timeouttest/TimeoutTestServer/docs/DelayresponseGet200Response.md b/test/server/timeouttest/TimeoutTestServer/docs/DelayresponseGet200Response.md deleted file mode 100644 index b47de34..0000000 --- a/test/server/timeouttest/TimeoutTestServer/docs/DelayresponseGet200Response.md +++ /dev/null @@ -1,12 +0,0 @@ -# DelayresponseGet200Response - - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**delay_seconds** | **String** | | [optional] [default to nothing] - - -[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) - - diff --git a/test/server/timeouttest/TimeoutTestServer/src/TimeoutTestServer.jl b/test/server/timeouttest/TimeoutTestServer/src/TimeoutTestServer.jl deleted file mode 100644 index 01e600b..0000000 --- a/test/server/timeouttest/TimeoutTestServer/src/TimeoutTestServer.jl +++ /dev/null @@ -1,57 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw""" -Encapsulates generated server code for TimeoutTestServer - -The following server methods must be implemented: - -- **delayresponse_get** - - *invocation:* GET /delayresponse - - *signature:* delayresponse_get(req::HTTP.Request, delay_seconds::Int64;) -> DelayresponseGet200Response -- **longpollstream_get** - - *invocation:* GET /longpollstream - - *signature:* longpollstream_get(req::HTTP.Request, delay_seconds::Int64;) -> DelayresponseGet200Response -""" -module TimeoutTestServer - -using HTTP -using URIs -using Dates -using TimeZones -using OpenAPI -using OpenAPI.Servers - -const API_VERSION = "1.0.0" - -include("modelincludes.jl") - -include("apis/api_DefaultApi.jl") - -""" -Register handlers for all APIs in this module in the supplied `Router` instance. - -Paramerets: -- `router`: Router to register handlers in -- `impl`: module that implements the server methods - -Optional parameters: -- `path_prefix`: prefix to be applied to all paths -- `optional_middlewares`: Register one or more optional middlewares to be applied to all requests. - -Optional middlewares can be one or more of: - - `init`: called before the request is processed - - `pre_validation`: called after the request is parsed but before validation - - `pre_invoke`: called after validation but before the handler is invoked - - `post_invoke`: called after the handler is invoked but before the response is sent - -The order in which middlewares are invoked are: -`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` -""" -function register(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - registerDefaultApi(router, impl; path_prefix=path_prefix, optional_middlewares...) - return router -end - -end # module TimeoutTestServer diff --git a/test/server/timeouttest/TimeoutTestServer/src/apis/api_DefaultApi.jl b/test/server/timeouttest/TimeoutTestServer/src/apis/api_DefaultApi.jl deleted file mode 100644 index 6a7ae51..0000000 --- a/test/server/timeouttest/TimeoutTestServer/src/apis/api_DefaultApi.jl +++ /dev/null @@ -1,82 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -function delayresponse_get_read(handler) - function delayresponse_get_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - query_params = HTTP.queryparams(URIs.URI(req.target)) - openapi_params["delay_seconds"] = OpenAPI.Servers.to_param(Int64, query_params, "delay_seconds", required=true, style="form", is_explode=true) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function delayresponse_get_validate(handler) - function delayresponse_get_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "delayresponse_get" - - n = "delay_seconds" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - OpenAPI.validate_param(n, op, :minimum, v, 0, false) - end - - return handler(req) - end -end - -function delayresponse_get_invoke(impl; post_invoke=nothing) - function delayresponse_get_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.delayresponse_get(req::HTTP.Request, openapi_params["delay_seconds"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function longpollstream_get_read(handler) - function longpollstream_get_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - query_params = HTTP.queryparams(URIs.URI(req.target)) - openapi_params["delay_seconds"] = OpenAPI.Servers.to_param(Int64, query_params, "delay_seconds", required=true, style="form", is_explode=true) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function longpollstream_get_validate(handler) - function longpollstream_get_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - op = "longpollstream_get" - - n = "delay_seconds" - v = get(openapi_params, n, nothing) - isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) - if !isnothing(v) - OpenAPI.validate_param(n, op, :minimum, v, 0, false) - end - - return handler(req) - end -end - -function longpollstream_get_invoke(impl; post_invoke=nothing) - function longpollstream_get_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.longpollstream_get(req::HTTP.Request, openapi_params["delay_seconds"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - - -function registerDefaultApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - HTTP.register!(router, "GET", path_prefix * "/delayresponse", OpenAPI.Servers.middleware(impl, delayresponse_get_read, delayresponse_get_validate, delayresponse_get_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/longpollstream", OpenAPI.Servers.middleware(impl, longpollstream_get_read, longpollstream_get_validate, longpollstream_get_invoke; optional_middlewares...)) - return router -end diff --git a/test/server/timeouttest/TimeoutTestServer/src/modelincludes.jl b/test/server/timeouttest/TimeoutTestServer/src/modelincludes.jl deleted file mode 100644 index af7c2e0..0000000 --- a/test/server/timeouttest/TimeoutTestServer/src/modelincludes.jl +++ /dev/null @@ -1,4 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -include("models/model_DelayresponseGet200Response.jl") diff --git a/test/server/timeouttest/TimeoutTestServer/src/models/model_DelayresponseGet200Response.jl b/test/server/timeouttest/TimeoutTestServer/src/models/model_DelayresponseGet200Response.jl deleted file mode 100644 index caa9121..0000000 --- a/test/server/timeouttest/TimeoutTestServer/src/models/model_DelayresponseGet200Response.jl +++ /dev/null @@ -1,36 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""_delayresponse_get_200_response - - DelayresponseGet200Response(; - delay_seconds=nothing, - ) - - - delay_seconds::String -""" -Base.@kwdef mutable struct DelayresponseGet200Response <: OpenAPI.APIModel - delay_seconds::Union{Nothing, String} = nothing - - function DelayresponseGet200Response(delay_seconds, ) - o = new(delay_seconds, ) - OpenAPI.validate_properties(o) - return o - end -end # type DelayresponseGet200Response - -const _property_types_DelayresponseGet200Response = Dict{Symbol,String}(Symbol("delay_seconds")=>"String", ) -OpenAPI.property_type(::Type{ DelayresponseGet200Response }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_DelayresponseGet200Response[name]))} - -function OpenAPI.check_required(o::DelayresponseGet200Response) - true -end - -function OpenAPI.validate_properties(o::DelayresponseGet200Response) - OpenAPI.validate_property(DelayresponseGet200Response, Symbol("delay_seconds"), o.delay_seconds) -end - -function OpenAPI.validate_property(::Type{ DelayresponseGet200Response }, name::Symbol, val) - -end diff --git a/test/server/timeouttest/generate.sh b/test/server/timeouttest/generate.sh deleted file mode 100755 index dac6975..0000000 --- a/test/server/timeouttest/generate.sh +++ /dev/null @@ -1,5 +0,0 @@ -java -jar openapi-generator-cli.jar generate \ - -i ../../specs/timeouttest.yaml \ - -g julia-server \ - -o TimeoutTestServer \ - --additional-properties=packageName=TimeoutTestServer diff --git a/test/server/timeouttest/timeouttest_server.jl b/test/server/timeouttest/timeouttest_server.jl deleted file mode 100644 index 2ac8dd9..0000000 --- a/test/server/timeouttest/timeouttest_server.jl +++ /dev/null @@ -1,68 +0,0 @@ -module TimeoutTestServerImpl - -using HTTP -using OpenAPI - -include("TimeoutTestServer/src/TimeoutTestServer.jl") - -using .TimeoutTestServer - -const server = Ref{Any}(nothing) - -""" -delayresponse_get - -*invocation:* GET /delayresponse -""" -function delayresponse_get(request::HTTP.Request) - delay_seconds = parse(Int, HTTP.URIs.queryparams(HTTP.URIs.parse_uri_reference(request.target))["delay_seconds"]) - sleep(delay_seconds) - return HTTP.Response(200, OpenAPI.Clients.to_json(TimeoutTestServer.DelayresponseGet200Response(string(delay_seconds)))) -end - -function stop(::HTTP.Request) - HTTP.close(server[]) - return HTTP.Response(200, "") -end - -function ping(::HTTP.Request) - return HTTP.Response(200, "") -end - -function longpollstream(stream::HTTP.Stream) - request::HTTP.Request = stream.message - - if startswith(request.target, "/longpollstream") - HTTP.setheader(stream, "Content-Type" => "application/json") - delay_seconds = parse(Int, HTTP.URIs.queryparams(HTTP.URIs.parse_uri_reference(request.target))["delay_seconds"]) - while true - write(stream, OpenAPI.Clients.to_json(TimeoutTestServer.DelayresponseGet200Response(string(delay_seconds)))) - write(stream, "\n") - sleep(delay_seconds) - end - end - return nothing -end - -function run_server(port=8081) - try - router = HTTP.Router() - HTTP.register!(router, "/delayresponse", HTTP.streamhandler(delayresponse_get)) - HTTP.register!(router, "/longpollstream", longpollstream) - HTTP.register!(router, "/stop", HTTP.streamhandler(stop)) - HTTP.register!(router, "/ping", HTTP.streamhandler(ping)) - # HTTP.jl 1.x serves stream handlers via `serve!(...; stream=true)`; - # 2.0 uses `listen!`, which always runs the handler in streaming mode. - http_v2 = isdefined(Base, :pkgversion) && something(pkgversion(HTTP), v"1") >= v"2" - server[] = http_v2 ? - HTTP.listen!(router, "127.0.0.1", port) : - HTTP.serve!(router, port; stream=true) - wait(server[]) - catch ex - @error("Server error", exception=(ex, catch_backtrace())) - end -end - -end # module TimeoutTestServerImpl - -TimeoutTestServerImpl.run_server() \ No newline at end of file diff --git a/test/specs/allany.yaml b/test/specs/allany.yaml deleted file mode 100644 index 8ef524c..0000000 --- a/test/specs/allany.yaml +++ /dev/null @@ -1,211 +0,0 @@ -openapi: 3.0.3 -info: - title: oneof anyof allof tests - description: |- - API to test code generation for oneof anyof allof - contact: - email: test@example.com - version: 0.0.1 -paths: - /echo_oneof_mapped_pets: - post: - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/OneOfMappedPets' - required: true - responses: - '200': - description: Successful response (echoes the request body) - content: - application/json: - schema: - $ref: '#/components/schemas/OneOfMappedPets' - /echo_oneof_pets: - post: - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/OneOfPets' - required: true - responses: - '200': - description: Successful response (echoes the request body) - content: - application/json: - schema: - $ref: '#/components/schemas/OneOfPets' - /echo_anyof_mapped_pets: - post: - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/AnyOfMappedPets' - required: true - responses: - '200': - description: Successful response (echoes the request body) - content: - application/json: - schema: - $ref: '#/components/schemas/AnyOfMappedPets' - /echo_anyof_pets: - post: - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/AnyOfPets' - required: true - responses: - '200': - description: Successful response (echoes the request body) - content: - application/json: - schema: - $ref: '#/components/schemas/AnyOfPets' - /echo_oneof_base_type: - post: - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/OneOfBaseType' - required: true - responses: - '200': - description: Successful response (echoes the request body) - content: - application/json: - schema: - $ref: '#/components/schemas/OneOfBaseType' - /echo_anyof_base_type: - post: - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/AnyOfBaseType' - required: true - responses: - '200': - description: Successful response (echoes the request body) - content: - application/json: - schema: - $ref: '#/components/schemas/AnyOfBaseType' - /echo_arrays: - post: - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/TypeWithAllArrayTypes' - required: true - responses: - '200': - description: Successful response (echoes the request body) - content: - application/json: - schema: - $ref: '#/components/schemas/TypeWithAllArrayTypes' -components: - schemas: - Pet: - type: object - required: - - pet_type - properties: - pet_type: - type: string - discriminator: - propertyName: pet_type - Dog: - allOf: - - $ref: '#/components/schemas/Pet' - - type: object - # all other properties specific to a `Dog` - properties: - bark: - type: boolean - breed: - type: string - enum: [Dingo, Husky, Retriever, Shepherd] - Cat: - allOf: - - $ref: '#/components/schemas/Pet' - - type: object - # all other properties specific to a `Cat` - properties: - hunts: - type: boolean - age: - type: integer - AnyOfPets: - anyOf: - - $ref: '#/components/schemas/Cat' - - $ref: '#/components/schemas/Dog' - discriminator: - propertyName: pet_type - AnyOfMappedPets: - anyOf: - - $ref: '#/components/schemas/Cat' - - $ref: '#/components/schemas/Dog' - discriminator: - propertyName: pet_type - mapping: - dog: '#/components/schemas/Dog' - cat: '#/components/schemas/Cat' - OneOfPets: - oneOf: - - $ref: '#/components/schemas/Cat' - - $ref: '#/components/schemas/Dog' - discriminator: - propertyName: pet_type - OneOfMappedPets: - oneOf: - - $ref: '#/components/schemas/Cat' - - $ref: '#/components/schemas/Dog' - discriminator: - propertyName: pet_type - mapping: - dog: '#/components/schemas/Dog' - cat: '#/components/schemas/Cat' - OneOfBaseType: - oneOf: - - type: string - - type: number - AnyOfBaseType: - anyOf: - - type: string - - type: number - ArrayOfOneOfBaseType: - type: array - items: - $ref: '#/components/schemas/OneOfBaseType' - ArrayOfAnyOfBaseType: - type: array - items: - $ref: '#/components/schemas/AnyOfBaseType' - ArrayOfOneOfPets: - type: array - items: - $ref: '#/components/schemas/OneOfPets' - ArrayOfAnyOfPets: - type: array - items: - $ref: '#/components/schemas/AnyOfPets' - TypeWithAllArrayTypes: - type: object - properties: - oneofbase: - $ref: '#/components/schemas/ArrayOfOneOfBaseType' - anyofbase: - $ref: '#/components/schemas/ArrayOfAnyOfBaseType' - oneofpets: - $ref: '#/components/schemas/ArrayOfOneOfPets' - anyofpets: - $ref: '#/components/schemas/ArrayOfAnyOfPets' diff --git a/test/specs/forms.json b/test/specs/forms.json deleted file mode 100644 index 327c63c..0000000 --- a/test/specs/forms.json +++ /dev/null @@ -1,195 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "Form POST and File Upload Tests", - "description": "Tests for different types of POST operations with forms and file uploads", - "version": "0.1.0" - }, - "servers": [ - ], - "paths": { - "/test/{form_id}/post_urlencoded_form_data": { - "post": { - "summary": "posts a urlencoded form, with file contents and additional metadata, both of which are strings", - "operationId": "postUrlencodedForm", - "parameters": [ - { - "name": "form_id", - "in": "path", - "description": "ID of form to update", - "required": true, - "style": "simple", - "explode": false, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/UrlencodedForm" - } - } - } - }, - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TestResponse" - } - } - } - } - } - } - }, - "/test/{file_id}/upload_binary_file": { - "post": { - "summary": "uploads a binary file given its path, along with some metadata", - "operationId": "uploadBinaryFile", - "parameters": [ - { - "name": "file_id", - "in": "path", - "description": "ID of file to update", - "required": true, - "style": "simple", - "explode": false, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/BinaryFileWithMetadata" - } - } - } - }, - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TestResponse" - } - } - } - } - }, - "x-accepts": "application/json", - "x-contentType": "multipart/form-data" - } - }, - "/test/{file_id}/upload_text_file": { - "post": { - "summary": "uploads text file contents along with some metadata", - "operationId": "uploadTextFile", - "parameters": [ - { - "name": "file_id", - "in": "path", - "description": "ID of file to update", - "required": true, - "style": "simple", - "explode": false, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/TextFileWithMetadata" - } - } - } - }, - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TestResponse" - } - } - } - } - }, - "x-accepts": "application/json", - "x-contentType": "multipart/form-data" - } - } - }, - "components": { - "schemas": { - "TestResponse": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - }, - "UrlencodedForm": { - "type": "object", - "properties": { - "additionalMetadata": { - "type": "string", - "description": "Additional data to pass to server" - }, - "file": { - "type": "string", - "description": "file contents to upload, in string format" - } - }, - "required": [ - "file" - ] - }, - "BinaryFileWithMetadata": { - "type": "object", - "properties": { - "additionalMetadata": { - "type": "string", - "description": "Additional data to pass to server" - }, - "file": { - "type": "string", - "description": "file to upload, must be a string representing a valid file path", - "format": "binary" - } - } - }, - "TextFileWithMetadata": { - "type": "object", - "properties": { - "additionalMetadata": { - "type": "string", - "description": "Additional data to pass to server, a string" - }, - "file": { - "type": "string", - "description": "file contents to upload in base64 encoded format", - "format": "base64" - } - } - } - } - } -} \ No newline at end of file diff --git a/test/specs/modelgen.json b/test/specs/modelgen.json deleted file mode 100644 index 805f1dd..0000000 --- a/test/specs/modelgen.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "Model Generation Tests", - "description": "Model Generation Tests", - "version": "0.1.0" - }, - "servers": [ - ], - "paths": { - "/test": { - "get": { - "summary": "Test", - "operationId": "test", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TestModel" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "TestModel": { - "type": "object", - "properties": { - "limited_by": { - "type": "string", - "default": "time", - "enum": ["time", "cost", "unlimited"] - }, - "default_date": { - "type": "string", - "format": "date", - "default": "2011-11-11" - }, - "default_datetime": { - "type": "string", - "format": "date-time", - "default": "2011-11-11T11:11:11Z" - }, - "max_val": { - "type": "integer", - "default": 100, - "enum": [100, 200, 300] - }, - "message": { - "type": "string", - "default": "success" - }, - "name": { - "type": "string", - "default": "new" - }, - "compute": { - "$ref": "#/components/schemas/ComputeType" - } - }, - "required": [ - "name" - ] - }, - "ComputeType": { - "type": "string", - "enum": ["cpu", "gpu"], - "description": "The compute type, either cpu or gpu" - } - } - } -} diff --git a/test/specs/openapigenerator_petstore_v3.json b/test/specs/openapigenerator_petstore_v3.json deleted file mode 100644 index 4958171..0000000 --- a/test/specs/openapigenerator_petstore_v3.json +++ /dev/null @@ -1,1132 +0,0 @@ -{ - "openapi": "3.0.0", - "servers": [ - { - "url": "/v3" - } - ], - "externalDocs": { - "description": "Find out more about Swagger", - "url": "http://swagger.io" - }, - "paths": { - "/user/logout": { - "get": { - "summary": "Logs out current logged in user session", - "responses": { - "default": { - "description": "successful operation" - } - }, - "operationId": "logoutUser", - "tags": [ - "user" - ], - "description": "", - "security": [ - { - "api_key": [] - } - ] - } - }, - "/store/order/{orderId}": { - "delete": { - "summary": "Delete purchase order by ID", - "parameters": [ - { - "name": "orderId", - "required": true, - "in": "path", - "description": "ID of the order that needs to be deleted", - "schema": { - "type": "string" - } - } - ], - "responses": { - "404": { - "description": "Order not found" - }, - "400": { - "description": "Invalid ID supplied" - } - }, - "operationId": "deleteOrder", - "tags": [ - "store" - ], - "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors" - }, - "get": { - "summary": "Find purchase order by ID", - "parameters": [ - { - "name": "orderId", - "required": true, - "in": "path", - "description": "ID of pet that needs to be fetched", - "schema": { - "minimum": 1, - "format": "int64", - "type": "integer", - "maximum": 5 - } - } - ], - "responses": { - "404": { - "description": "Order not found" - }, - "400": { - "description": "Invalid ID supplied" - }, - "200": { - "content": { - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - }, - "description": "successful operation" - } - }, - "operationId": "getOrderById", - "tags": [ - "store" - ], - "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions" - } - }, - "/user/{username}": { - "delete": { - "summary": "Delete user", - "parameters": [ - { - "name": "username", - "required": true, - "in": "path", - "description": "The name that needs to be deleted", - "schema": { - "type": "string" - } - } - ], - "responses": { - "404": { - "description": "User not found" - }, - "400": { - "description": "Invalid username supplied" - } - }, - "operationId": "deleteUser", - "tags": [ - "user" - ], - "description": "This can only be done by the logged in user.", - "security": [ - { - "api_key": [] - } - ] - }, - "put": { - "summary": "Updated user", - "parameters": [ - { - "name": "username", - "required": true, - "in": "path", - "description": "name that need to be deleted", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - }, - "required": true, - "description": "Updated user object" - }, - "responses": { - "404": { - "description": "User not found" - }, - "400": { - "description": "Invalid user supplied" - } - }, - "operationId": "updateUser", - "tags": [ - "user" - ], - "description": "This can only be done by the logged in user.", - "security": [ - { - "api_key": [] - } - ] - }, - "get": { - "summary": "Get user by user name", - "parameters": [ - { - "name": "username", - "required": true, - "in": "path", - "description": "The name that needs to be fetched. Use user1 for testing.", - "schema": { - "type": "string" - } - } - ], - "responses": { - "404": { - "description": "User not found" - }, - "400": { - "description": "Invalid username supplied" - }, - "200": { - "content": { - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - }, - "description": "successful operation" - } - }, - "operationId": "getUserByName", - "tags": [ - "user" - ], - "description": "" - } - }, - "/user/login": { - "get": { - "summary": "Logs user into the system", - "parameters": [ - { - "name": "username", - "required": true, - "in": "query", - "description": "The user name for login", - "schema": { - "pattern": "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$", - "type": "string" - } - }, - { - "name": "password", - "required": true, - "in": "query", - "description": "The password for login in clear text", - "schema": { - "type": "string" - } - } - ], - "responses": { - "400": { - "description": "Invalid username/password supplied" - }, - "200": { - "headers": { - "Set-Cookie": { - "description": "Cookie authentication key for use with the `api_key` apiKey authentication.", - "schema": { - "example": "AUTH_KEY=abcde12345; Path=/; HttpOnly", - "type": "string" - } - }, - "X-Rate-Limit": { - "description": "calls per hour allowed by the user", - "schema": { - "format": "int32", - "type": "integer" - } - }, - "X-Expires-After": { - "description": "date in UTC when token expires", - "schema": { - "format": "date-time", - "type": "string" - } - } - }, - "content": { - "application/xml": { - "schema": { - "type": "string" - } - }, - "application/json": { - "schema": { - "type": "string" - } - } - }, - "description": "successful operation" - } - }, - "operationId": "loginUser", - "tags": [ - "user" - ], - "description": "" - } - }, - "/pet/{petId}/uploadImage": { - "post": { - "summary": "uploads an image", - "parameters": [ - { - "name": "petId", - "required": true, - "in": "path", - "description": "ID of pet to update", - "schema": { - "format": "int64", - "type": "integer" - } - } - ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "properties": { - "file": { - "format": "binary", - "description": "file to upload", - "type": "string" - }, - "additionalMetadata": { - "description": "Additional data to pass to server", - "type": "string" - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponse" - } - } - }, - "description": "successful operation" - } - }, - "operationId": "uploadFile", - "tags": [ - "pet" - ], - "description": "", - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/store/inventory": { - "get": { - "summary": "Returns pet inventories by status", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "additionalProperties": { - "format": "int32", - "type": "integer" - }, - "type": "object" - } - } - }, - "description": "successful operation" - } - }, - "operationId": "getInventory", - "tags": [ - "store" - ], - "description": "Returns a map of status codes to quantities", - "security": [ - { - "api_key": [] - } - ] - } - }, - "/pet": { - "put": { - "summary": "Update an existing pet", - "requestBody": { - "$ref": "#/components/requestBodies/Pet" - }, - "externalDocs": { - "url": "http://petstore.swagger.io/v2/doc/updatePet", - "description": "API documentation for the updatePet operation" - }, - "operationId": "updatePet", - "responses": { - "405": { - "description": "Validation exception" - }, - "404": { - "description": "Pet not found" - }, - "400": { - "description": "Invalid ID supplied" - }, - "200": { - "content": { - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "description": "successful operation" - } - }, - "tags": [ - "pet" - ], - "description": "", - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - }, - "post": { - "summary": "Add a new pet to the store", - "requestBody": { - "$ref": "#/components/requestBodies/Pet" - }, - "responses": { - "405": { - "description": "Invalid input" - }, - "200": { - "content": { - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "description": "successful operation" - } - }, - "operationId": "addPet", - "tags": [ - "pet" - ], - "description": "", - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/user": { - "post": { - "summary": "Create user", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - }, - "required": true, - "description": "Created user object" - }, - "responses": { - "default": { - "description": "successful operation" - } - }, - "operationId": "createUser", - "tags": [ - "user" - ], - "description": "This can only be done by the logged in user.", - "security": [ - { - "api_key": [] - } - ] - } - }, - "/user/createWithArray": { - "post": { - "summary": "Creates list of users with given input array", - "requestBody": { - "$ref": "#/components/requestBodies/UserArray" - }, - "responses": { - "default": { - "description": "successful operation" - } - }, - "operationId": "createUsersWithArrayInput", - "tags": [ - "user" - ], - "description": "", - "security": [ - { - "api_key": [] - } - ] - } - }, - "/pet/findByStatus": { - "get": { - "summary": "Finds Pets by status", - "parameters": [ - { - "schema": { - "items": { - "default": "available", - "type": "string", - "enum": [ - "available", - "pending", - "sold" - ] - }, - "type": "array" - }, - "name": "status", - "required": true, - "style": "form", - "in": "query", - "description": "Status values that need to be considered for filter", - "explode": false, - "deprecated": true - } - ], - "responses": { - "400": { - "description": "Invalid status value" - }, - "200": { - "content": { - "application/xml": { - "schema": { - "items": { - "$ref": "#/components/schemas/Pet" - }, - "type": "array" - } - }, - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Pet" - }, - "type": "array" - } - } - }, - "description": "successful operation" - } - }, - "operationId": "findPetsByStatus", - "tags": [ - "pet" - ], - "description": "Multiple status values can be provided with comma separated strings", - "security": [ - { - "petstore_auth": [ - "read:pets" - ] - } - ] - } - }, - "/user/createWithList": { - "post": { - "summary": "Creates list of users with given input array", - "requestBody": { - "$ref": "#/components/requestBodies/UserArray" - }, - "responses": { - "default": { - "description": "successful operation" - } - }, - "operationId": "createUsersWithListInput", - "tags": [ - "user" - ], - "description": "", - "security": [ - { - "api_key": [] - } - ] - } - }, - "/pet/{petId}": { - "delete": { - "summary": "Deletes a pet", - "parameters": [ - { - "name": "api_key", - "required": false, - "in": "header", - "schema": { - "type": "string" - } - }, - { - "name": "petId", - "required": true, - "in": "path", - "description": "Pet id to delete", - "schema": { - "format": "int64", - "type": "integer" - } - } - ], - "responses": { - "400": { - "description": "Invalid pet value" - } - }, - "operationId": "deletePet", - "tags": [ - "pet" - ], - "description": "", - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - }, - "post": { - "summary": "Updates a pet in the store with form data", - "parameters": [ - { - "name": "petId", - "required": true, - "in": "path", - "description": "ID of pet that needs to be updated", - "schema": { - "format": "int64", - "type": "integer" - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "properties": { - "name": { - "description": "Updated name of the pet", - "type": "string" - }, - "status": { - "description": "Updated status of the pet", - "type": "string" - } - }, - "type": "object" - } - } - } - }, - "responses": { - "405": { - "description": "Invalid input" - } - }, - "operationId": "updatePetWithForm", - "tags": [ - "pet" - ], - "description": "", - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - }, - "get": { - "summary": "Find pet by ID", - "parameters": [ - { - "name": "petId", - "required": true, - "in": "path", - "description": "ID of pet to return", - "schema": { - "format": "int64", - "type": "integer" - } - } - ], - "responses": { - "404": { - "description": "Pet not found" - }, - "400": { - "description": "Invalid ID supplied" - }, - "200": { - "content": { - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "description": "successful operation" - } - }, - "operationId": "getPetById", - "tags": [ - "pet" - ], - "description": "Returns a single pet", - "security": [ - { - "api_key": [] - } - ] - } - }, - "/pet/findByTags": { - "get": { - "summary": "Finds Pets by tags", - "parameters": [ - { - "name": "tags", - "required": true, - "style": "form", - "in": "query", - "description": "Tags to filter by", - "explode": false, - "schema": { - "items": { - "type": "string" - }, - "type": "array" - } - } - ], - "responses": { - "400": { - "description": "Invalid tag value" - }, - "200": { - "content": { - "application/xml": { - "schema": { - "items": { - "$ref": "#/components/schemas/Pet" - }, - "type": "array" - } - }, - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Pet" - }, - "type": "array" - } - } - }, - "description": "successful operation" - } - }, - "operationId": "findPetsByTags", - "tags": [ - "pet" - ], - "deprecated": true, - "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", - "security": [ - { - "petstore_auth": [ - "read:pets" - ] - } - ] - } - }, - "/store/order": { - "post": { - "summary": "Place an order for a pet", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - }, - "required": true, - "description": "order placed for purchasing the pet" - }, - "responses": { - "400": { - "description": "Invalid Order" - }, - "200": { - "content": { - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - }, - "description": "successful operation" - } - }, - "operationId": "placeOrder", - "tags": [ - "store" - ], - "description": "" - } - } - }, - "tags": [ - { - "name": "pet", - "description": "Everything about your Pets" - }, - { - "name": "store", - "description": "Access to Petstore orders" - }, - { - "name": "user", - "description": "Operations about user" - } - ], - "info": { - "title": "OpenAPI Petstore", - "license": { - "name": "Apache-2.0", - "url": "https://www.apache.org/licenses/LICENSE-2.0.html" - }, - "description": "This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.", - "version": "1.0.0" - }, - "components": { - "securitySchemes": { - "petstore_auth": { - "flows": { - "implicit": { - "authorizationUrl": "http://petstore.swagger.io/api/oauth/dialog", - "scopes": { - "read:pets": "read your pets", - "write:pets": "modify pets in your account" - } - } - }, - "type": "oauth2" - }, - "api_key": { - "name": "api_key", - "in": "header", - "type": "apiKey" - } - }, - "requestBodies": { - "UserArray": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/User" - }, - "type": "array" - } - } - }, - "required": true, - "description": "List of user object" - }, - "Pet": { - "content": { - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "required": true, - "description": "Pet object that needs to be added to the store" - } - }, - "schemas": { - "User": { - "xml": { - "name": "User" - }, - "properties": { - "password": { - "type": "string" - }, - "id": { - "format": "int64", - "type": "integer" - }, - "username": { - "type": "string" - }, - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "userStatus": { - "format": "int32", - "type": "integer", - "description": "User Status" - }, - "email": { - "type": "string" - } - }, - "title": "a User", - "description": "A User who is purchasing from the pet store", - "type": "object" - }, - "Order": { - "xml": { - "name": "Order" - }, - "properties": { - "petId": { - "format": "int64", - "type": "integer" - }, - "shipDate": { - "format": "date-time", - "type": "string" - }, - "status": { - "type": "string", - "description": "Order Status", - "enum": [ - "placed", - "approved", - "delivered" - ] - }, - "id": { - "format": "int64", - "type": "integer" - }, - "complete": { - "default": false, - "type": "boolean" - }, - "quantity": { - "format": "int32", - "type": "integer" - } - }, - "title": "Pet Order", - "description": "An order for a pets from the pet store", - "type": "object" - }, - "Pet": { - "xml": { - "name": "Pet" - }, - "required": [ - "name", - "photoUrls" - ], - "properties": { - "name": { - "example": "doggie", - "type": "string" - }, - "status": { - "deprecated": true, - "type": "string", - "description": "pet status in the store", - "enum": [ - "available", - "pending", - "sold" - ] - }, - "id": { - "format": "int64", - "type": "integer" - }, - "photoUrls": { - "xml": { - "name": "photoUrl", - "wrapped": true - }, - "items": { - "type": "string" - }, - "type": "array" - }, - "tags": { - "xml": { - "name": "tag", - "wrapped": true - }, - "items": { - "$ref": "#/components/schemas/Tag" - }, - "type": "array" - }, - "category": { - "$ref": "#/components/schemas/Category" - } - }, - "title": "a Pet", - "description": "A pet for sale in the pet store", - "type": "object" - }, - "Category": { - "xml": { - "name": "Category" - }, - "properties": { - "name": { - "pattern": "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$", - "type": "string" - }, - "id": { - "format": "int64", - "type": "integer" - } - }, - "title": "Pet category", - "description": "A category for a pet", - "type": "object" - }, - "Tag": { - "xml": { - "name": "Tag" - }, - "properties": { - "name": { - "type": "string" - }, - "id": { - "format": "int64", - "type": "integer" - } - }, - "title": "Pet Tag", - "description": "A tag for a pet", - "type": "object" - }, - "ApiResponse": { - "properties": { - "message": { - "type": "string" - }, - "code": { - "format": "int32", - "type": "integer" - }, - "type": { - "type": "string" - } - }, - "title": "An uploaded response", - "description": "Describes the result of uploading an image resource", - "type": "object" - } - } - } -} diff --git a/test/specs/petstore_v2.json b/test/specs/petstore_v2.json deleted file mode 100644 index daeb7f6..0000000 --- a/test/specs/petstore_v2.json +++ /dev/null @@ -1,1054 +0,0 @@ -{ - "swagger": "2.0", - "info": { - "description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.", - "version": "1.0.6", - "title": "Swagger Petstore", - "termsOfService": "http://swagger.io/terms/", - "contact": { - "email": "apiteam@swagger.io" - }, - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - } - }, - "host": "petstore.swagger.io", - "basePath": "/v2", - "tags": [ - { - "name": "pet", - "description": "Everything about your Pets", - "externalDocs": { - "description": "Find out more", - "url": "http://swagger.io" - } - }, - { - "name": "store", - "description": "Access to Petstore orders" - }, - { - "name": "user", - "description": "Operations about user", - "externalDocs": { - "description": "Find out more about our store", - "url": "http://swagger.io" - } - } - ], - "schemes": [ - "https", - "http" - ], - "paths": { - "/pet/{petId}/uploadImage": { - "post": { - "tags": [ - "pet" - ], - "summary": "uploads an image", - "description": "", - "operationId": "uploadFile", - "consumes": [ - "multipart/form-data" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "petId", - "in": "path", - "description": "ID of pet to update", - "required": true, - "type": "integer", - "format": "int64" - }, - { - "name": "additionalMetadata", - "in": "formData", - "description": "Additional data to pass to server", - "required": false, - "type": "string" - }, - { - "name": "file", - "in": "formData", - "description": "file to upload", - "required": false, - "type": "file" - } - ], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "$ref": "#/definitions/ApiResponse" - } - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/pet": { - "post": { - "tags": [ - "pet" - ], - "summary": "Add a new pet to the store", - "description": "", - "operationId": "addPet", - "consumes": [ - "application/json", - "application/xml" - ], - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "Pet object that needs to be added to the store", - "required": true, - "schema": { - "$ref": "#/definitions/Pet" - } - } - ], - "responses": { - "405": { - "description": "Invalid input" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - }, - "put": { - "tags": [ - "pet" - ], - "summary": "Update an existing pet", - "description": "", - "operationId": "updatePet", - "consumes": [ - "application/json", - "application/xml" - ], - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "Pet object that needs to be added to the store", - "required": true, - "schema": { - "$ref": "#/definitions/Pet" - } - } - ], - "responses": { - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Pet not found" - }, - "405": { - "description": "Validation exception" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/pet/findByStatus": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by status", - "description": "Multiple status values can be provided with comma separated strings", - "operationId": "findPetsByStatus", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "status", - "in": "query", - "description": "Status values that need to be considered for filter", - "required": true, - "type": "array", - "items": { - "type": "string", - "enum": [ - "available", - "pending", - "sold" - ], - "default": "available" - }, - "collectionFormat": "multi" - } - ], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/Pet" - } - } - }, - "400": { - "description": "Invalid status value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/pet/findByTags": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by tags", - "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", - "operationId": "findPetsByTags", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "tags", - "in": "query", - "description": "Tags to filter by", - "required": true, - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi" - } - ], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/Pet" - } - } - }, - "400": { - "description": "Invalid tag value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ], - "deprecated": true - } - }, - "/pet/{petId}": { - "get": { - "tags": [ - "pet" - ], - "summary": "Find pet by ID", - "description": "Returns a single pet", - "operationId": "getPetById", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "petId", - "in": "path", - "description": "ID of pet to return", - "required": true, - "type": "integer", - "format": "int64" - } - ], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "$ref": "#/definitions/Pet" - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Pet not found" - } - }, - "security": [ - { - "api_key": [] - } - ] - }, - "post": { - "tags": [ - "pet" - ], - "summary": "Updates a pet in the store with form data", - "description": "", - "operationId": "updatePetWithForm", - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "petId", - "in": "path", - "description": "ID of pet that needs to be updated", - "required": true, - "type": "integer", - "format": "int64" - }, - { - "name": "name", - "in": "formData", - "description": "Updated name of the pet", - "required": false, - "type": "string" - }, - { - "name": "status", - "in": "formData", - "description": "Updated status of the pet", - "required": false, - "type": "string" - } - ], - "responses": { - "405": { - "description": "Invalid input" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - }, - "delete": { - "tags": [ - "pet" - ], - "summary": "Deletes a pet", - "description": "", - "operationId": "deletePet", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "api_key", - "in": "header", - "required": false, - "type": "string" - }, - { - "name": "petId", - "in": "path", - "description": "Pet id to delete", - "required": true, - "type": "integer", - "format": "int64" - } - ], - "responses": { - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Pet not found" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/store/order": { - "post": { - "tags": [ - "store" - ], - "summary": "Place an order for a pet", - "description": "", - "operationId": "placeOrder", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "order placed for purchasing the pet", - "required": true, - "schema": { - "$ref": "#/definitions/Order" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "$ref": "#/definitions/Order" - } - }, - "400": { - "description": "Invalid Order" - } - } - } - }, - "/store/order/{orderId}": { - "get": { - "tags": [ - "store" - ], - "summary": "Find purchase order by ID", - "description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions", - "operationId": "getOrderById", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of pet that needs to be fetched", - "required": true, - "type": "integer", - "maximum": 10, - "minimum": 1, - "format": "int64" - } - ], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "$ref": "#/definitions/Order" - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - }, - "delete": { - "tags": [ - "store" - ], - "summary": "Delete purchase order by ID", - "description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors", - "operationId": "deleteOrder", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of the order that needs to be deleted", - "required": true, - "type": "integer", - "minimum": 1, - "format": "int64" - } - ], - "responses": { - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - } - }, - "/store/inventory": { - "get": { - "tags": [ - "store" - ], - "summary": "Returns pet inventories by status", - "description": "Returns a map of status codes to quantities", - "operationId": "getInventory", - "produces": [ - "application/json" - ], - "parameters": [], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int32" - } - } - } - }, - "security": [ - { - "api_key": [] - } - ] - } - }, - "/user/createWithArray": { - "post": { - "tags": [ - "user" - ], - "summary": "Creates list of users with given input array", - "description": "", - "operationId": "createUsersWithArrayInput", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "List of user object", - "required": true, - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/User" - } - } - } - ], - "responses": { - "default": { - "description": "successful operation" - } - } - } - }, - "/user/createWithList": { - "post": { - "tags": [ - "user" - ], - "summary": "Creates list of users with given input array", - "description": "", - "operationId": "createUsersWithListInput", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "List of user object", - "required": true, - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/User" - } - } - } - ], - "responses": { - "default": { - "description": "successful operation" - } - } - } - }, - "/user/{username}": { - "get": { - "tags": [ - "user" - ], - "summary": "Get user by user name", - "description": "", - "operationId": "getUserByName", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be fetched. Use user1 for testing. ", - "required": true, - "type": "string" - } - ], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "$ref": "#/definitions/User" - } - }, - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - }, - "put": { - "tags": [ - "user" - ], - "summary": "Updated user", - "description": "This can only be done by the logged in user.", - "operationId": "updateUser", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "username", - "in": "path", - "description": "name that need to be updated", - "required": true, - "type": "string" - }, - { - "in": "body", - "name": "body", - "description": "Updated user object", - "required": true, - "schema": { - "$ref": "#/definitions/User" - } - } - ], - "responses": { - "400": { - "description": "Invalid user supplied" - }, - "404": { - "description": "User not found" - } - } - }, - "delete": { - "tags": [ - "user" - ], - "summary": "Delete user", - "description": "This can only be done by the logged in user.", - "operationId": "deleteUser", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be deleted", - "required": true, - "type": "string" - } - ], - "responses": { - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - } - }, - "/user/login": { - "get": { - "tags": [ - "user" - ], - "summary": "Logs user into the system", - "description": "", - "operationId": "loginUser", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "username", - "in": "query", - "description": "The user name for login", - "required": true, - "type": "string" - }, - { - "name": "password", - "in": "query", - "description": "The password for login in clear text", - "required": true, - "type": "string" - } - ], - "responses": { - "200": { - "description": "successful operation", - "headers": { - "X-Expires-After": { - "type": "string", - "format": "date-time", - "description": "date in UTC when token expires" - }, - "X-Rate-Limit": { - "type": "integer", - "format": "int32", - "description": "calls per hour allowed by the user" - } - }, - "schema": { - "type": "string" - } - }, - "400": { - "description": "Invalid username/password supplied" - } - } - } - }, - "/user/logout": { - "get": { - "tags": [ - "user" - ], - "summary": "Logs out current logged in user session", - "description": "", - "operationId": "logoutUser", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [], - "responses": { - "default": { - "description": "successful operation" - } - } - } - }, - "/user": { - "post": { - "tags": [ - "user" - ], - "summary": "Create user", - "description": "This can only be done by the logged in user.", - "operationId": "createUser", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "Created user object", - "required": true, - "schema": { - "$ref": "#/definitions/User" - } - } - ], - "responses": { - "default": { - "description": "successful operation" - } - } - } - } - }, - "securityDefinitions": { - "api_key": { - "type": "apiKey", - "name": "api_key", - "in": "header" - }, - "petstore_auth": { - "type": "oauth2", - "authorizationUrl": "https://petstore.swagger.io/oauth/authorize", - "flow": "implicit", - "scopes": { - "read:pets": "read your pets", - "write:pets": "modify pets in your account" - } - } - }, - "definitions": { - "ApiResponse": { - "type": "object", - "properties": { - "code": { - "type": "integer", - "format": "int32" - }, - "type": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "Category": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string" - } - }, - "xml": { - "name": "Category" - } - }, - "Pet": { - "type": "object", - "required": [ - "name", - "photoUrls" - ], - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "category": { - "$ref": "#/definitions/Category" - }, - "name": { - "type": "string", - "example": "doggie" - }, - "photoUrls": { - "type": "array", - "xml": { - "wrapped": true - }, - "items": { - "type": "string", - "xml": { - "name": "photoUrl" - } - } - }, - "tags": { - "type": "array", - "xml": { - "wrapped": true - }, - "items": { - "xml": { - "name": "tag" - }, - "$ref": "#/definitions/Tag" - } - }, - "status": { - "type": "string", - "description": "pet status in the store", - "enum": [ - "available", - "pending", - "sold" - ] - } - }, - "xml": { - "name": "Pet" - } - }, - "Tag": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string" - } - }, - "xml": { - "name": "Tag" - } - }, - "Order": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "petId": { - "type": "integer", - "format": "int64" - }, - "quantity": { - "type": "integer", - "format": "int32" - }, - "shipDate": { - "type": "string", - "format": "date-time" - }, - "status": { - "type": "string", - "description": "Order Status", - "enum": [ - "placed", - "approved", - "delivered" - ] - }, - "complete": { - "type": "boolean" - } - }, - "xml": { - "name": "Order" - } - }, - "User": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "username": { - "type": "string" - }, - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "email": { - "type": "string" - }, - "password": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "userStatus": { - "type": "integer", - "format": "int32", - "description": "User Status" - } - }, - "xml": { - "name": "User" - } - } - }, - "externalDocs": { - "description": "Find out more about Swagger", - "url": "http://swagger.io" - } -} diff --git a/test/specs/petstore_v3.json b/test/specs/petstore_v3.json deleted file mode 100644 index 8da19c1..0000000 --- a/test/specs/petstore_v3.json +++ /dev/null @@ -1,1265 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "OpenAPI Petstore", - "description": "This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. For OAuth2 flow, you may use `user` as both username and password when asked to login.", - "license": { - "name": "Apache-2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - }, - "version": "1.0.0" - }, - "externalDocs": { - "description": "Find out more about OpenAPI generator", - "url": "https://openapi-generator.tech" - }, - "servers": [ - { - "url": "/v3" - } - ], - "tags": [ - { - "name": "pet", - "description": "Everything about your Pets" - }, - { - "name": "store", - "description": "Access to Petstore orders" - }, - { - "name": "user", - "description": "Operations about user" - } - ], - "paths": { - "/pet": { - "put": { - "tags": [ - "pet" - ], - "summary": "Update an existing pet", - "operationId": "updatePet", - "requestBody": { - "$ref": "#/components/requestBodies/Pet" - }, - "responses": { - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Pet not found" - }, - "405": { - "description": "Validation exception" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ], - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "pet" - } - ], - "x-contentType": "application/json" - }, - "post": { - "tags": [ - "pet" - ], - "summary": "Add a new pet to the store", - "operationId": "addPet", - "requestBody": { - "$ref": "#/components/requestBodies/Pet" - }, - "responses": { - "405": { - "description": "Invalid input" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ], - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "pet" - } - ], - "x-contentType": "application/json" - } - }, - "/pet/findByStatus": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by status", - "description": "Multiple status values can be provided with comma separated strings", - "operationId": "findPetsByStatus", - "parameters": [ - { - "name": "status", - "in": "query", - "description": "Status values that need to be considered for filter", - "required": true, - "style": "form", - "explode": false, - "schema": { - "type": "array", - "items": { - "type": "string", - "default": "available", - "enum": [ - "available", - "pending", - "sold" - ] - } - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/xml": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - }, - "400": { - "description": "Invalid status value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ], - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "pet" - } - ] - } - }, - "/pet/findByTags": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by tags", - "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", - "operationId": "findPetsByTags", - "parameters": [ - { - "name": "tags", - "in": "query", - "description": "Tags to filter by", - "required": true, - "style": "form", - "explode": false, - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/xml": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - }, - "400": { - "description": "Invalid tag value" - } - }, - "deprecated": true, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ], - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "pet" - } - ] - } - }, - "/pet/{petId}": { - "get": { - "tags": [ - "pet" - ], - "summary": "Find pet by ID", - "description": "Returns a single pet", - "operationId": "getPetById", - "parameters": [ - { - "name": "petId", - "in": "path", - "description": "ID of pet to return", - "required": true, - "style": "simple", - "explode": false, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Pet not found" - } - }, - "security": [ - { - "api_key": [] - } - ], - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "pet" - } - ] - }, - "post": { - "tags": [ - "pet" - ], - "summary": "Updates a pet in the store with form data", - "operationId": "updatePetWithForm", - "parameters": [ - { - "name": "petId", - "in": "path", - "description": "ID of pet that needs to be updated", - "required": true, - "style": "simple", - "explode": false, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/body" - } - } - } - }, - "responses": { - "405": { - "description": "Invalid input" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ], - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "pet" - } - ], - "x-contentType": "application/x-www-form-urlencoded" - }, - "delete": { - "tags": [ - "pet" - ], - "summary": "Deletes a pet", - "operationId": "deletePet", - "parameters": [ - { - "name": "api_key", - "in": "header", - "required": false, - "style": "simple", - "explode": false, - "schema": { - "type": "string" - } - }, - { - "name": "petId", - "in": "path", - "description": "Pet id to delete", - "required": true, - "style": "simple", - "explode": false, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "400": { - "description": "Invalid pet value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ], - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "pet" - } - ] - } - }, - "/pet/{petId}/uploadImage": { - "post": { - "tags": [ - "pet" - ], - "summary": "uploads an image", - "operationId": "uploadFile", - "parameters": [ - { - "name": "petId", - "in": "path", - "description": "ID of pet to update", - "required": true, - "style": "simple", - "explode": false, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/body_1" - } - } - } - }, - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponse" - } - } - } - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ], - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "pet" - } - ], - "x-contentType": "multipart/form-data" - } - }, - "/store/inventory": { - "get": { - "tags": [ - "store" - ], - "summary": "Returns pet inventories by status", - "description": "Returns a map of status codes to quantities", - "operationId": "getInventory", - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int32" - } - } - } - } - } - }, - "security": [ - { - "api_key": [] - } - ], - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "store" - } - ] - } - }, - "/store/order": { - "post": { - "tags": [ - "store" - ], - "summary": "Place an order for a pet", - "operationId": "placeOrder", - "requestBody": { - "description": "order placed for purchasing the pet", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "400": { - "description": "Invalid Order" - } - }, - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "store" - } - ], - "x-contentType": "application/json" - } - }, - "/store/order/{orderId}": { - "get": { - "tags": [ - "store" - ], - "summary": "Find purchase order by ID", - "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions", - "operationId": "getOrderById", - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of pet that needs to be fetched", - "required": true, - "style": "simple", - "explode": false, - "schema": { - "maximum": 5, - "minimum": 1, - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - }, - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "store" - } - ] - }, - "delete": { - "tags": [ - "store" - ], - "summary": "Delete purchase order by ID", - "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", - "operationId": "deleteOrder", - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of the order that needs to be deleted", - "required": true, - "style": "simple", - "explode": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - }, - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "store" - } - ] - } - }, - "/user": { - "post": { - "tags": [ - "user" - ], - "summary": "Create user", - "description": "This can only be done by the logged in user.", - "operationId": "createUser", - "requestBody": { - "description": "Created user object", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - }, - "required": true - }, - "responses": { - "default": { - "description": "successful operation" - } - }, - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "user" - } - ], - "x-contentType": "application/json" - } - }, - "/user/createWithArray": { - "post": { - "tags": [ - "user" - ], - "summary": "Creates list of users with given input array", - "operationId": "createUsersWithArrayInput", - "requestBody": { - "$ref": "#/components/requestBodies/UserArray" - }, - "responses": { - "default": { - "description": "successful operation" - } - }, - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "user" - } - ], - "x-contentType": "application/json" - } - }, - "/user/createWithList": { - "post": { - "tags": [ - "user" - ], - "summary": "Creates list of users with given input array", - "operationId": "createUsersWithListInput", - "requestBody": { - "$ref": "#/components/requestBodies/UserArray" - }, - "responses": { - "default": { - "description": "successful operation" - } - }, - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "user" - } - ], - "x-contentType": "application/json" - } - }, - "/user/login": { - "get": { - "tags": [ - "user" - ], - "summary": "Logs user into the system", - "operationId": "loginUser", - "parameters": [ - { - "name": "username", - "in": "query", - "description": "The user name for login", - "required": true, - "style": "form", - "explode": true, - "schema": { - "type": "string" - } - }, - { - "name": "password", - "in": "query", - "description": "The password for login in clear text", - "required": true, - "style": "form", - "explode": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "headers": { - "X-Rate-Limit": { - "description": "calls per hour allowed by the user", - "style": "simple", - "explode": false, - "schema": { - "type": "integer", - "format": "int32" - } - }, - "X-Expires-After": { - "description": "date in UTC when toekn expires", - "style": "simple", - "explode": false, - "schema": { - "type": "string", - "format": "date-time" - } - } - }, - "content": { - "application/xml": { - "schema": { - "type": "string" - } - }, - "application/json": { - "schema": { - "type": "string" - } - } - } - }, - "400": { - "description": "Invalid username/password supplied" - } - }, - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "user" - } - ] - } - }, - "/user/logout": { - "get": { - "tags": [ - "user" - ], - "summary": "Logs out current logged in user session", - "operationId": "logoutUser", - "responses": { - "default": { - "description": "successful operation" - } - }, - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "user" - } - ] - } - }, - "/user/{username}": { - "get": { - "tags": [ - "user" - ], - "summary": "Get user by user name", - "operationId": "getUserByName", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be fetched. Use user1 for testing.", - "required": true, - "style": "simple", - "explode": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - }, - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "user" - } - ] - }, - "put": { - "tags": [ - "user" - ], - "summary": "Updated user", - "description": "This can only be done by the logged in user.", - "operationId": "updateUser", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "name that need to be deleted", - "required": true, - "style": "simple", - "explode": false, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "description": "Updated user object", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - }, - "required": true - }, - "responses": { - "400": { - "description": "Invalid user supplied" - }, - "404": { - "description": "User not found" - } - }, - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "user" - } - ], - "x-contentType": "application/json" - }, - "delete": { - "tags": [ - "user" - ], - "summary": "Delete user", - "description": "This can only be done by the logged in user.", - "operationId": "deleteUser", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be deleted", - "required": true, - "style": "simple", - "explode": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - }, - "x-accepts": "application/json", - "x-tags": [ - { - "tag": "user" - } - ] - } - } - }, - "components": { - "schemas": { - "Order": { - "title": "Pet Order", - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "petId": { - "type": "integer", - "format": "int64" - }, - "quantity": { - "type": "integer", - "format": "int32" - }, - "shipDate": { - "type": "string", - "format": "date-time" - }, - "status": { - "type": "string", - "description": "Order Status", - "enum": [ - "placed", - "approved", - "delivered" - ] - }, - "complete": { - "type": "boolean", - "default": false - } - }, - "description": "An order for a pets from the pet store", - "example": { - "petId": 6, - "quantity": 1, - "id": 0, - "shipDate": "2000-01-23T04:56:07.000+00:00", - "complete": false, - "status": "placed" - }, - "xml": { - "name": "Order" - } - }, - "Category": { - "title": "Pet category", - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string" - } - }, - "description": "A category for a pet", - "example": { - "name": "name", - "id": 6 - }, - "xml": { - "name": "Category" - } - }, - "User": { - "title": "a User", - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "username": { - "type": "string" - }, - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "email": { - "type": "string" - }, - "password": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "userStatus": { - "type": "integer", - "description": "User Status", - "format": "int32" - } - }, - "description": "A User who is purchasing from the pet store", - "example": { - "firstName": "firstName", - "lastName": "lastName", - "password": "password", - "userStatus": 6, - "phone": "phone", - "id": 0, - "email": "email", - "username": "username" - }, - "xml": { - "name": "User" - } - }, - "Tag": { - "title": "Pet Tag", - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string" - } - }, - "description": "A tag for a pet", - "example": { - "name": "name", - "id": 1 - }, - "xml": { - "name": "Tag" - } - }, - "Pet": { - "title": "a Pet", - "required": [ - "name", - "photoUrls" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "category": { - "$ref": "#/components/schemas/Category" - }, - "name": { - "type": "string", - "example": "doggie" - }, - "photoUrls": { - "type": "array", - "xml": { - "name": "photoUrl", - "wrapped": true - }, - "items": { - "type": "string" - } - }, - "tags": { - "type": "array", - "xml": { - "name": "tag", - "wrapped": true - }, - "items": { - "$ref": "#/components/schemas/Tag" - } - }, - "status": { - "type": "string", - "description": "pet status in the store", - "enum": [ - "available", - "pending", - "sold" - ] - } - }, - "description": "A pet for sale in the pet store", - "example": { - "photoUrls": [ - "photoUrls", - "photoUrls" - ], - "name": "doggie", - "id": 0, - "category": { - "name": "name", - "id": 6 - }, - "tags": [ - { - "name": "name", - "id": 1 - }, - { - "name": "name", - "id": 1 - } - ], - "status": "available" - }, - "xml": { - "name": "Pet" - } - }, - "ApiResponse": { - "title": "An uploaded response", - "type": "object", - "properties": { - "code": { - "type": "integer", - "format": "int32" - }, - "type": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "description": "Describes the result of uploading an image resource", - "example": { - "code": 0, - "type": "type", - "message": "message" - } - }, - "body": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Updated name of the pet" - }, - "status": { - "type": "string", - "description": "Updated status of the pet" - } - } - }, - "body_1": { - "type": "object", - "properties": { - "additionalMetadata": { - "type": "string", - "description": "Additional data to pass to server" - }, - "file": { - "type": "string", - "description": "file to upload", - "format": "binary" - } - } - } - }, - "requestBodies": { - "UserArray": { - "description": "List of user object", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "required": true - }, - "Pet": { - "description": "Pet object that needs to be added to the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "required": true - } - }, - "securitySchemes": { - "petstore_auth": { - "type": "oauth2", - "flows": { - "implicit": { - "authorizationUrl": "/api/oauth/dialog", - "scopes": { - "write:pets": "modify pets in your account", - "read:pets": "read your pets" - } - } - } - }, - "api_key": { - "type": "apiKey", - "name": "api_key", - "in": "header" - } - } - } -} diff --git a/test/specs/stresstest.yaml b/test/specs/stresstest.yaml deleted file mode 100644 index 5d634d2..0000000 --- a/test/specs/stresstest.yaml +++ /dev/null @@ -1,56 +0,0 @@ -openapi: 3.0.3 -info: - title: Stress Test Echo Service - description: Simple echo service for stress testing the OpenAPI client - version: 1.0.0 -servers: - - url: http://127.0.0.1:8082 - description: Local test server -paths: - /echo: - get: - summary: Echo GET endpoint - description: Returns a simple JSON response with server timestamp - responses: - '200': - description: Successful response - content: - application/json: - schema: - type: object - properties: - timestamp: - type: string - format: date-time - description: Server timestamp when request was received - message: - type: string - description: Echo message - post: - summary: Echo POST endpoint - description: Echoes back the request body with metadata - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - data: - type: string - description: Data to echo back - responses: - '200': - description: Successful response - content: - application/json: - schema: - type: object - properties: - timestamp: - type: string - format: date-time - description: Server timestamp when request was received - data: - type: string - description: Echoed data from request diff --git a/test/specs/timeouttest.yaml b/test/specs/timeouttest.yaml deleted file mode 100644 index 7888c12..0000000 --- a/test/specs/timeouttest.yaml +++ /dev/null @@ -1,55 +0,0 @@ -openapi: 3.0.3 -info: - title: Timeout Test Service - version: 1.0.0 -paths: - /delayresponse: - get: - summary: Delay Response Endpoint - parameters: - - name: delay_seconds - in: query - description: Number of seconds to delay the response - required: true - schema: - type: integer - minimum: 0 - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - delay_seconds: - type: string - x-code-samples: - - lang: curl - source: | - curl -X GET "http://example.com/delayresponse?delay_seconds=5" - /longpollstream: - get: - summary: Long polled streaming endpoint - parameters: - - name: delay_seconds - in: query - description: Number of seconds to delay the response - required: true - schema: - type: integer - minimum: 0 - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - delay_seconds: - type: string - x-code-samples: - - lang: curl - source: | - curl -X GET "http://example.com/longpollstream?delay_seconds=5" \ No newline at end of file diff --git a/test/streaming_latency_tests.jl b/test/streaming_latency_tests.jl deleted file mode 100644 index 3d366e3..0000000 --- a/test/streaming_latency_tests.jl +++ /dev/null @@ -1,110 +0,0 @@ -module StreamingLatencyTests - -using Test -using Sockets - -import OpenAPI -import OpenAPI.Clients: Client, Ctx, exec, LineChunkReader - -# Streamed chunks must be forwarded to the consumer as soon as they arrive, -# not held back until an internal read buffer fills. Regression test for the -# :http backend stalling small chunks: `readbytes!(io, buf)` blocks until the -# whole 8KB buffer is filled, so a chunk smaller than that (e.g. a Kubernetes -# watch event) was only delivered once enough later data accumulated — on a -# quiet stream, effectively never. -# -# The server is a minimal raw-TCP chunked HTTP/1.1 responder so that the test -# controls exactly when bytes hit the wire, independent of HTTP.jl's server -# API (which differs between 1.x and 2.x). Two endpoints: -# /quick - both chunks back to back (used to warm up/compile the client path) -# /stall - first chunk immediately, second only after `delay` seconds - -# JSON string literals: in the streaming path the client parses each chunk as -# JSON (no response object to take a content-type from), so a quoted string -# round-trips to a `String` return value. -const CHUNK1 = "\"tick-1\"\n" -const CHUNK2 = "\"tick-2\"\n" - -function write_chunk(sock, data) - write(sock, string(sizeof(data), base=16), "\r\n", data, "\r\n") - flush(sock) -end - -function handle_connection(sock, delay) - try - request_line = readline(sock) - path = split(request_line, ' ')[2] - while true - line = readline(sock) - (isempty(line) || line == "\r") && break - end - # `connection: close` so the client does not try to reuse the socket, - # which this bare-bones server closes after each response - write(sock, "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\nconnection: close\r\ntransfer-encoding: chunked\r\n\r\n") - flush(sock) - write_chunk(sock, CHUNK1) - endswith(path, "/stall") && sleep(delay) - write_chunk(sock, CHUNK2) - write(sock, "0\r\n\r\n") - flush(sock) - catch - # client went away; nothing to do - finally - close(sock) - end -end - -function start_server(delay) - listener = Sockets.listen(Sockets.localhost, 0) - _, port = Sockets.getsockname(listener) - @async begin - while true - sock = try - accept(listener) - catch - break # listener closed - end - @async handle_connection(sock, delay) - end - end - listener, Int(port) -end - -# Run a streaming GET and return (first chunk, seconds until it arrived, -# remaining chunks). -function first_chunk_latency(httplib, port, path) - client = Client("http://127.0.0.1:$port"; httplib=httplib) - return_types = Dict{Regex,Type}(Regex("^200\$") => String) - ctx = Ctx(client, "GET", return_types, path, String[]; chunk_reader_type=LineChunkReader) - events = Channel{Any}(64) - t0 = time() - task = @async exec(ctx, events) - first = take!(events) - latency = time() - t0 - remaining = collect(events) - wait(task) - (first, latency, remaining) -end - -function runtests() - delay = 4.0 - listener, port = start_server(delay) - try - @testset "first chunk not delayed until buffer fills ($httplib)" for httplib in values(OpenAPI.Clients.HTTPLib) - # warm up: compile the full streaming request path so the timing - # below measures I/O behavior, not JIT - first_chunk_latency(httplib, port, "/quick") - - first, latency, remaining = first_chunk_latency(httplib, port, "/stall") - @test first == "tick-1" - # the first chunk must arrive while the server is still stalling - # before the second chunk, not when the response completes - @test latency < delay / 2 - @test remaining == ["tick-2"] - end - finally - close(listener) - end -end - -end # module StreamingLatencyTests diff --git a/test/stresstest/README.md b/test/stresstest/README.md deleted file mode 100644 index 0f021d5..0000000 --- a/test/stresstest/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Stress Test for OpenAPI.jl Client - -Stress testing suite for the OpenAPI.jl HTTP client. - -## Quick Start - -Run the stress test with default settings: - -```bash -julia runtests.jl -``` - -## Configuration - -Configure via environment variables: - -| Variable | Default | Description | -|----------|---------|-------------| -| `STRESS_DURATION` | 30 | Test duration in seconds | -| `STRESS_CONCURRENCY` | 10 | Number of concurrent tasks | -| `STRESS_PAYLOAD_SIZE` | 1024 | POST payload size in bytes | -| `STRESS_HTTPLIB` | http | HTTP backend (`http` or `downloads`) | - -## Examples - -**Light test:** -```bash -STRESS_DURATION=5 STRESS_CONCURRENCY=5 julia runtests.jl -``` - -**Test with Downloads.jl backend:** -```bash -STRESS_HTTPLIB=downloads STRESS_DURATION=30 STRESS_CONCURRENCY=100 julia runtests.jl -``` diff --git a/test/stresstest/StressTest/StressTest.jl b/test/stresstest/StressTest/StressTest.jl deleted file mode 100644 index e191961..0000000 --- a/test/stresstest/StressTest/StressTest.jl +++ /dev/null @@ -1,24 +0,0 @@ -module StressTest - -using JSON -using Statistics -using Printf - -# Include submodules in correct dependency order -include("metrics.jl") # Independent, must come first -include("execution.jl") # Depends on metrics.jl - -# Re-export all public functions and types -export StressMetrics, - record_success, record_error, - report_metrics, - calculate_percentile, - get_total_requests, get_success_count, - get_success_rate, get_error_rate, get_throughput, - get_min_latency, get_max_latency, - get_mean_latency, get_median_latency, - StressTestConfig, - run_get_stress_test, run_post_stress_test, - generate_payload - -end # module diff --git a/test/stresstest/StressTest/execution.jl b/test/stresstest/StressTest/execution.jl deleted file mode 100644 index 7b7ebc5..0000000 --- a/test/stresstest/StressTest/execution.jl +++ /dev/null @@ -1,134 +0,0 @@ -""" -Stress test execution functions for testing the OpenAPI client. -""" - -""" - StressTestConfig - -Configuration for stress tests. - -# Fields -- `duration::Int` - Test duration in seconds -- `concurrency::Int` - Number of concurrent tasks -- `payload_size::Int` - POST payload size in bytes -- `httplib::Symbol` - HTTP backend to use (:http or :downloads) -- `target_url::String` - Base URL of the echo server -""" -mutable struct StressTestConfig - duration::Int - concurrency::Int - payload_size::Int - httplib::Symbol - target_url::String - - function StressTestConfig(; - duration=30, - concurrency=100, - payload_size=1024, - httplib=:http, - target_url="http://127.0.0.1:8082", - ) - return new(duration, concurrency, payload_size, httplib, target_url) - end -end - -function generate_payload(size::Int) - data = "x" ^ max(1, size) - return Main.StressTestClient.EchoPostRequest(; data = data) -end - -""" - run_get_stress_test(api::DefaultApi, config::StressTestConfig, metrics::StressMetrics) - -Run a GET stress test for the specified duration and concurrency. - -# Arguments -- `api::DefaultApi` - API instance to use -- `config::StressTestConfig` - Test configuration -- `metrics::StressMetrics` - Metrics container to record results -""" -function run_get_stress_test(api::Main.StressTestClient.DefaultApi, config::StressTestConfig, metrics::StressMetrics) - @info("Starting GET stress test") - - metrics.start_time = time() - - @sync begin - for task_idx in 1:config.concurrency - @async begin - task_start = time() - local requests_made = 0 - - while time() - task_start < config.duration - try - t0 = time() - result, resp = Main.StressTestClient.echo_get(api) - duration = time() - t0 - - if resp.status == 200 - record_success(metrics, duration) - else - record_error(metrics, "HTTP-$(resp.status)") - end - - requests_made += 1 - catch ex - error_type = string(typeof(ex)) - # Remove module prefix for cleaner output - error_type = split(error_type, ".")[end] - record_error(metrics, error_type) - end - end - end - end - end - - metrics.end_time = time() -end - -""" - run_post_stress_test(api::DefaultApi, config::StressTestConfig, metrics::StressMetrics) - -Run a POST stress test for the specified duration and concurrency. - -# Arguments -- `api::DefaultApi` - API instance to use -- `config::StressTestConfig` - Test configuration -- `metrics::StressMetrics` - Metrics container to record results -""" -function run_post_stress_test(api::Main.StressTestClient.DefaultApi, config::StressTestConfig, metrics::StressMetrics) - @info("Starting POST stress test") - payload = generate_payload(config.payload_size) - metrics.start_time = time() - - @sync begin - for task_idx in 1:config.concurrency - @async begin - task_start = time() - local requests_made = 0 - - while time() - task_start < config.duration - try - t0 = time() - result, resp = Main.StressTestClient.echo_post(api, payload) - duration = time() - t0 - - if resp.status == 200 - record_success(metrics, duration) - else - record_error(metrics, "HTTP-$(resp.status)") - end - - requests_made += 1 - catch ex - error_type = string(typeof(ex)) - # Remove module prefix for cleaner output - error_type = split(error_type, ".")[end] - record_error(metrics, error_type) - end - end - end - end - end - - metrics.end_time = time() -end diff --git a/test/stresstest/StressTest/metrics.jl b/test/stresstest/StressTest/metrics.jl deleted file mode 100644 index 11d8b45..0000000 --- a/test/stresstest/StressTest/metrics.jl +++ /dev/null @@ -1,306 +0,0 @@ -""" -Metrics collection and reporting for stress tests. -""" - -using Statistics -using Printf - -""" - StressMetrics - -Container for collecting metrics during stress tests. - -# Fields -- `request_times::Vector{Float64}` - Response time for each request (in seconds) -- `error_count::Int` - Total number of failed requests -- `error_types::Dict{String,Int}` - Count of each error type -- `start_time::Float64` - Test start time (from time()) -- `end_time::Float64` - Test end time (from time()) -""" -mutable struct StressMetrics - request_times::Vector{Float64} - error_count::Int - error_types::Dict{String,Int} - start_time::Float64 - end_time::Float64 - - function StressMetrics() - return new(Float64[], 0, Dict{String,Int}(), 0.0, 0.0) - end -end - -""" - record_success(metrics::StressMetrics, duration::Float64) - -Record a successful request with the given duration. - -# Arguments -- `metrics::StressMetrics` - Metrics container -- `duration::Float64` - Request duration in seconds -""" -function record_success(metrics::StressMetrics, duration::Float64) - push!(metrics.request_times, duration) -end - -""" - record_error(metrics::StressMetrics, error_type::String) - -Record a failed request with the given error type. - -# Arguments -- `metrics::StressMetrics` - Metrics container -- `error_type::String` - Type or description of the error -""" -function record_error(metrics::StressMetrics, error_type::String) - metrics.error_count += 1 - metrics.error_types[error_type] = get(metrics.error_types, error_type, 0) + 1 -end - -""" - calculate_percentile(times::Vector{Float64}, p::Float64)::Float64 - -Calculate the p-th percentile of the given times. - -# Arguments -- `times::Vector{Float64}` - Vector of times (in seconds) -- `p::Float64` - Percentile (0-100) - -# Returns -The p-th percentile value in seconds, or 0.0 if no data -""" -function calculate_percentile(times::Vector{Float64}, p::Float64)::Float64 - if isempty(times) - return 0.0 - end - - sorted_times = sort(times) - index = ceil(Int, length(sorted_times) * p / 100) - index = max(1, min(index, length(sorted_times))) - return sorted_times[index] -end - -""" - get_total_requests(metrics::StressMetrics)::Int - -Get the total number of requests (successful + failed). -""" -function get_total_requests(metrics::StressMetrics)::Int - return length(metrics.request_times) + metrics.error_count -end - -""" - get_success_count(metrics::StressMetrics)::Int - -Get the number of successful requests. -""" -function get_success_count(metrics::StressMetrics)::Int - return length(metrics.request_times) -end - -""" - get_success_rate(metrics::StressMetrics)::Float64 - -Get the success rate as a percentage (0-100). -""" -function get_success_rate(metrics::StressMetrics)::Float64 - total = get_total_requests(metrics) - if total == 0 - return 0.0 - end - return 100.0 * get_success_count(metrics) / total -end - -""" - get_error_rate(metrics::StressMetrics)::Float64 - -Get the error rate as a percentage (0-100). -""" -function get_error_rate(metrics::StressMetrics)::Float64 - return 100.0 - get_success_rate(metrics) -end - -""" - get_throughput(metrics::StressMetrics)::Float64 - -Get the throughput in requests per second. -""" -function get_throughput(metrics::StressMetrics)::Float64 - if metrics.start_time == 0.0 || metrics.end_time == 0.0 - return 0.0 - end - - duration = metrics.end_time - metrics.start_time - if duration <= 0.0 - return 0.0 - end - - return get_total_requests(metrics) / duration -end - -""" - get_min_latency(metrics::StressMetrics)::Float64 - -Get the minimum request latency in seconds. -""" -function get_min_latency(metrics::StressMetrics)::Float64 - if isempty(metrics.request_times) - return 0.0 - end - return minimum(metrics.request_times) -end - -""" - get_max_latency(metrics::StressMetrics)::Float64 - -Get the maximum request latency in seconds. -""" -function get_max_latency(metrics::StressMetrics)::Float64 - if isempty(metrics.request_times) - return 0.0 - end - return maximum(metrics.request_times) -end - -""" - get_mean_latency(metrics::StressMetrics)::Float64 - -Get the mean request latency in seconds. -""" -function get_mean_latency(metrics::StressMetrics)::Float64 - if isempty(metrics.request_times) - return 0.0 - end - return mean(metrics.request_times) -end - -""" - get_median_latency(metrics::StressMetrics)::Float64 - -Get the median request latency in seconds. -""" -function get_median_latency(metrics::StressMetrics)::Float64 - if isempty(metrics.request_times) - return 0.0 - end - return median(metrics.request_times) -end - -""" - report_metrics(metrics::StressMetrics, endpoint::String, payload_size::Union{Int,Nothing}=nothing) - -Print a formatted report of the collected metrics. - -# Arguments -- `metrics::StressMetrics` - Metrics container -- `endpoint::String` - The endpoint that was tested (e.g., "GET /echo") -- `payload_size::Union{Int,Nothing}` - Payload size in bytes (optional, for POST requests) -""" -function report_metrics(metrics::StressMetrics, endpoint::String, payload_size::Union{Int,Nothing}=nothing) - println("\n" * "="^60) - println("$endpoint Results") - if payload_size !== nothing - println("Payload Size: $(format_bytes(payload_size))") - end - println("="^60) - - total = get_total_requests(metrics) - success = get_success_count(metrics) - success_rate = get_success_rate(metrics) - error_rate = get_error_rate(metrics) - - println(" Total Requests: $(format_number(total))") - println(" Successful: $(format_number(success)) ($(format_percent(success_rate))%)") - println(" Failed: $(metrics.error_count) ($(format_percent(error_rate))%)") - - if metrics.error_count > 0 - println("\n Error Types:") - for (error_type, count) in sort(collect(metrics.error_types), by=x -> -x[2]) - percent = 100.0 * count / metrics.error_count - println(" $error_type: $count ($(format_percent(percent))%)") - end - end - - throughput = get_throughput(metrics) - println("\n Throughput: $(format_number(throughput)) req/s") - - if !isempty(metrics.request_times) - println("\n Latency:") - println(" Min: $(format_latency(get_min_latency(metrics)))") - println(" Mean: $(format_latency(get_mean_latency(metrics)))") - println(" Median: $(format_latency(get_median_latency(metrics)))") - println(" Max: $(format_latency(get_max_latency(metrics)))") - println(" P95: $(format_latency(calculate_percentile(metrics.request_times, 95.0)))") - println(" P99: $(format_latency(calculate_percentile(metrics.request_times, 99.0)))") - end - - if metrics.start_time > 0.0 && metrics.end_time > 0.0 - duration = metrics.end_time - metrics.start_time - println("\n Duration: $(format_number(duration))s") - end - println() -end - -# Formatting helper functions - -""" - format_number(n::Union{Int,Float64})::String - -Format a number with thousand separators. -""" -function format_number(n::Union{Int,Float64})::String - if n isa Int - # Format integer with commas as thousand separators - s = string(n) - parts = [] - for (i, c) in enumerate(reverse(s)) - if i > 1 && (i - 1) % 3 == 0 - pushfirst!(parts, ",") - end - pushfirst!(parts, c) - end - return join(parts) - else - return @sprintf("%.1f", n) - end -end - -""" - format_percent(p::Float64)::String - -Format a percentage with one decimal place. -""" -function format_percent(p::Float64)::String - return @sprintf("%.1f", p) -end - -""" - format_latency(seconds::Float64)::String - -Format latency in appropriate units (ms or seconds). -""" -function format_latency(seconds::Float64)::String - if seconds < 0.001 - return @sprintf("%.2fμs", seconds * 1e6) - elseif seconds < 1.0 - return @sprintf("%.2fms", seconds * 1000) - else - return @sprintf("%.2fs", seconds) - end -end - -""" - format_bytes(bytes::Int)::String - -Format bytes in appropriate units (B, KB, MB, GB). -""" -function format_bytes(bytes::Int)::String - if bytes < 1024 - return "$(bytes)B" - elseif bytes < 1024 * 1024 - return @sprintf("%.1fKB", bytes / 1024) - elseif bytes < 1024 * 1024 * 1024 - return @sprintf("%.1fMB", bytes / (1024 * 1024)) - else - return @sprintf("%.1fGB", bytes / (1024 * 1024 * 1024)) - end -end diff --git a/test/stresstest/StressTestClient/src/StressTestClient.jl b/test/stresstest/StressTestClient/src/StressTestClient.jl deleted file mode 100644 index 6dbbd38..0000000 --- a/test/stresstest/StressTestClient/src/StressTestClient.jl +++ /dev/null @@ -1,16 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -module StressTestClient - -using Dates, TimeZones -using OpenAPI -using OpenAPI.Clients - -const API_VERSION = "1.0.0" - -include("modelincludes.jl") - -include("apis/api_DefaultApi.jl") - -end # module StressTestClient diff --git a/test/stresstest/StressTestClient/src/apis/api_DefaultApi.jl b/test/stresstest/StressTestClient/src/apis/api_DefaultApi.jl deleted file mode 100644 index 1bac822..0000000 --- a/test/stresstest/StressTestClient/src/apis/api_DefaultApi.jl +++ /dev/null @@ -1,74 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -struct DefaultApi <: OpenAPI.APIClientImpl - client::OpenAPI.Clients.Client -end - -""" -The default API base path for APIs in `DefaultApi`. -This can be used to construct the `OpenAPI.Clients.Client` instance. -""" -basepath(::Type{ DefaultApi }) = "http://127.0.0.1:8082" - -const _returntypes_echo_get_DefaultApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => EchoGet200Response, -) - -function _oacinternal_echo_get(_api::DefaultApi; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "GET", _returntypes_echo_get_DefaultApi, "/echo", []) - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? [] : [_mediaType]) - return _ctx -end - -@doc raw"""Echo GET endpoint - -Returns a simple JSON response with server timestamp - -Params: - -Return: EchoGet200Response, OpenAPI.Clients.ApiResponse -""" -function echo_get(_api::DefaultApi; _mediaType=nothing) - _ctx = _oacinternal_echo_get(_api; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function echo_get(_api::DefaultApi, response_stream::Channel; _mediaType=nothing) - _ctx = _oacinternal_echo_get(_api; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -const _returntypes_echo_post_DefaultApi = Dict{Regex,Type}( - Regex("^" * replace("200", "x"=>".") * "\$") => EchoPost200Response, -) - -function _oacinternal_echo_post(_api::DefaultApi, echo_post_request::EchoPostRequest; _mediaType=nothing) - _ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_echo_post_DefaultApi, "/echo", [], echo_post_request) - OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ]) - OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["application/json", ] : [_mediaType]) - return _ctx -end - -@doc raw"""Echo POST endpoint - -Echoes back the request body with metadata - -Params: -- echo_post_request::EchoPostRequest (required) - -Return: EchoPost200Response, OpenAPI.Clients.ApiResponse -""" -function echo_post(_api::DefaultApi, echo_post_request::EchoPostRequest; _mediaType=nothing) - _ctx = _oacinternal_echo_post(_api, echo_post_request; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx) -end - -function echo_post(_api::DefaultApi, response_stream::Channel, echo_post_request::EchoPostRequest; _mediaType=nothing) - _ctx = _oacinternal_echo_post(_api, echo_post_request; _mediaType=_mediaType) - return OpenAPI.Clients.exec(_ctx, response_stream) -end - -export echo_get -export echo_post diff --git a/test/stresstest/StressTestClient/src/modelincludes.jl b/test/stresstest/StressTestClient/src/modelincludes.jl deleted file mode 100644 index 12bd391..0000000 --- a/test/stresstest/StressTestClient/src/modelincludes.jl +++ /dev/null @@ -1,6 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - -include("models/model_EchoGet200Response.jl") -include("models/model_EchoPost200Response.jl") -include("models/model_EchoPostRequest.jl") diff --git a/test/stresstest/StressTestClient/src/models/model_EchoGet200Response.jl b/test/stresstest/StressTestClient/src/models/model_EchoGet200Response.jl deleted file mode 100644 index da622a6..0000000 --- a/test/stresstest/StressTestClient/src/models/model_EchoGet200Response.jl +++ /dev/null @@ -1,44 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""_echo_get_200_response - - EchoGet200Response(; - timestamp=nothing, - message=nothing, - ) - - - timestamp::ZonedDateTime : Server timestamp when request was received - - message::String : Echo message -""" -Base.@kwdef mutable struct EchoGet200Response <: OpenAPI.APIModel - timestamp::Union{Nothing, ZonedDateTime} = nothing - message::Union{Nothing, String} = nothing - - function EchoGet200Response(timestamp, message, ) - o = new(timestamp, message, ) - OpenAPI.validate_properties(o) - return o - end -end # type EchoGet200Response - -const _property_types_EchoGet200Response = Dict{Symbol,String}(Symbol("timestamp")=>"ZonedDateTime", Symbol("message")=>"String", ) -OpenAPI.property_type(::Type{ EchoGet200Response }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_EchoGet200Response[name]))} - -function OpenAPI.check_required(o::EchoGet200Response) - true -end - -function OpenAPI.validate_properties(o::EchoGet200Response) - OpenAPI.validate_property(EchoGet200Response, Symbol("timestamp"), o.timestamp) - OpenAPI.validate_property(EchoGet200Response, Symbol("message"), o.message) -end - -function OpenAPI.validate_property(::Type{ EchoGet200Response }, name::Symbol, val) - - if name === Symbol("timestamp") - OpenAPI.validate_param(name, "EchoGet200Response", :format, val, "date-time") - end - -end diff --git a/test/stresstest/StressTestClient/src/models/model_EchoPost200Response.jl b/test/stresstest/StressTestClient/src/models/model_EchoPost200Response.jl deleted file mode 100644 index 7eee139..0000000 --- a/test/stresstest/StressTestClient/src/models/model_EchoPost200Response.jl +++ /dev/null @@ -1,44 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""_echo_post_200_response - - EchoPost200Response(; - timestamp=nothing, - data=nothing, - ) - - - timestamp::ZonedDateTime : Server timestamp when request was received - - data::String : Echoed data from request -""" -Base.@kwdef mutable struct EchoPost200Response <: OpenAPI.APIModel - timestamp::Union{Nothing, ZonedDateTime} = nothing - data::Union{Nothing, String} = nothing - - function EchoPost200Response(timestamp, data, ) - o = new(timestamp, data, ) - OpenAPI.validate_properties(o) - return o - end -end # type EchoPost200Response - -const _property_types_EchoPost200Response = Dict{Symbol,String}(Symbol("timestamp")=>"ZonedDateTime", Symbol("data")=>"String", ) -OpenAPI.property_type(::Type{ EchoPost200Response }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_EchoPost200Response[name]))} - -function OpenAPI.check_required(o::EchoPost200Response) - true -end - -function OpenAPI.validate_properties(o::EchoPost200Response) - OpenAPI.validate_property(EchoPost200Response, Symbol("timestamp"), o.timestamp) - OpenAPI.validate_property(EchoPost200Response, Symbol("data"), o.data) -end - -function OpenAPI.validate_property(::Type{ EchoPost200Response }, name::Symbol, val) - - if name === Symbol("timestamp") - OpenAPI.validate_param(name, "EchoPost200Response", :format, val, "date-time") - end - -end diff --git a/test/stresstest/StressTestClient/src/models/model_EchoPostRequest.jl b/test/stresstest/StressTestClient/src/models/model_EchoPostRequest.jl deleted file mode 100644 index b68f6ad..0000000 --- a/test/stresstest/StressTestClient/src/models/model_EchoPostRequest.jl +++ /dev/null @@ -1,36 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -@doc raw"""_echo_post_request - - EchoPostRequest(; - data=nothing, - ) - - - data::String : Data to echo back -""" -Base.@kwdef mutable struct EchoPostRequest <: OpenAPI.APIModel - data::Union{Nothing, String} = nothing - - function EchoPostRequest(data, ) - o = new(data, ) - OpenAPI.validate_properties(o) - return o - end -end # type EchoPostRequest - -const _property_types_EchoPostRequest = Dict{Symbol,String}(Symbol("data")=>"String", ) -OpenAPI.property_type(::Type{ EchoPostRequest }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_EchoPostRequest[name]))} - -function OpenAPI.check_required(o::EchoPostRequest) - true -end - -function OpenAPI.validate_properties(o::EchoPostRequest) - OpenAPI.validate_property(EchoPostRequest, Symbol("data"), o.data) -end - -function OpenAPI.validate_property(::Type{ EchoPostRequest }, name::Symbol, val) - -end diff --git a/test/stresstest/generate.sh b/test/stresstest/generate.sh deleted file mode 100755 index ce22532..0000000 --- a/test/stresstest/generate.sh +++ /dev/null @@ -1,5 +0,0 @@ -java -jar openapi-generator-cli.jar generate \ - -i ../specs/stresstest.yaml \ - -g julia-client \ - -o StressTestClient \ - --additional-properties=packageName=StressTestClient diff --git a/test/stresstest/runtests.jl b/test/stresstest/runtests.jl deleted file mode 100644 index dd97c14..0000000 --- a/test/stresstest/runtests.jl +++ /dev/null @@ -1,90 +0,0 @@ -""" -Stress tests for OpenAPI.jl client testing. - -Environment Variables: - STRESS_DURATION - Test duration in seconds (default: 10) - STRESS_CONCURRENCY - Number of concurrent tasks (default: 10) - STRESS_PAYLOAD_SIZE - POST payload size in bytes (default: 1024) - STRESS_HTTPLIB - HTTP backend to use, :http or :downloads (default: :http) - -Example: - STRESS_DURATION=30 STRESS_CONCURRENCY=10 julia --project=.. runtests.jl -""" - -using Test -using OpenAPI -using OpenAPI.Clients -using HTTP - -include("../testutils.jl") -include("StressTestClient/src/StressTestClient.jl") -using .StressTestClient -include("StressTest/StressTest.jl") -using .StressTest - -const TEST_PORT = 8082 -const TEST_SERVER_SCRIPT = abspath(joinpath(@__DIR__, "stresstest_server.jl")) - -""" -Parse configuration from environment variables with defaults. -""" -function get_config() - duration = parse(Int, get(ENV, "STRESS_DURATION", "30")) - concurrency = parse(Int, get(ENV, "STRESS_CONCURRENCY", "10")) - payload_size = parse(Int, get(ENV, "STRESS_PAYLOAD_SIZE", "1024")) - - httplib_str = get(ENV, "STRESS_HTTPLIB", "http") - httplib = Symbol(httplib_str) - if !in(httplib, (:http, :downloads)) - @warn("Invalid STRESS_HTTPLIB '$httplib_str', using :http") - httplib = :http - end - - return StressTestConfig( - duration=duration, - concurrency=concurrency, - payload_size=payload_size, - httplib=httplib, - ) -end - -function main() - config = get_config() - - @info("Starting stress test", - server_port=TEST_PORT, - duration = config.duration, - concurrency = config.concurrency, - http_backend = config.httplib, - - ) - proc, iob = run_server(TEST_SERVER_SCRIPT) - - try - if !wait_server(TEST_PORT) - @error("Server failed to start") - return false - end - - client = OpenAPI.Clients.Client(config.target_url; httplib=config.httplib) - api = StressTestClient.DefaultApi(client) - - get_metrics = StressMetrics() - run_get_stress_test(api, config, get_metrics) - report_metrics(get_metrics, "GET /echo") - - post_metrics = StressMetrics() - run_post_stress_test(api, config, post_metrics) - report_metrics(post_metrics, "POST /echo", config.payload_size) - - return true - finally - @info("Stopping server") - stop_server(TEST_PORT, proc, iob) - end -end - -@testset "Stress Tests" begin - success = main() - @test success -end diff --git a/test/stresstest/stresstest_server.jl b/test/stresstest/stresstest_server.jl deleted file mode 100644 index 1ca6d29..0000000 --- a/test/stresstest/stresstest_server.jl +++ /dev/null @@ -1,100 +0,0 @@ -module StressTestServerImpl - -using HTTP -using OpenAPI -using Dates -using Random -using JSON - -const server = Ref{Any}(nothing) -const headers = ["Content-Type" => "application/json"] - - -""" - echo_get(request::HTTP.Request) - -Handler for GET /echo endpoint. -Returns a simple JSON response with timestamp and request info. -""" -function echo_get(request::HTTP.Request) - timestamp = Dates.now(UTC) - - response_data = Dict( - "timestamp" => string(timestamp), - "message" => "Echo GET response", - ) - - return HTTP.Response(200, headers, JSON.json(response_data)) -end - -""" - echo_post(request::HTTP.Request) - -Handler for POST /echo endpoint. -Echoes back the request body with metadata. -""" -function echo_post(request::HTTP.Request) - timestamp = Dates.now(UTC) - request_body = String(request.body) - request_data = JSON.parse(request_body) - - response_data = Dict( - "timestamp" => string(timestamp), - "data" => get(request_data, "data", ""), - ) - - response_json = JSON.json(response_data) - - return HTTP.Response(200, headers, response_json) -end - -""" - stop(::HTTP.Request) - -Handler for GET /stop endpoint. -Gracefully shuts down the server. -""" -function stop(::HTTP.Request) - try - HTTP.close(server[]) - catch - # Ignore errors during shutdown - end - return HTTP.Response(200, "") -end - -""" - ping(::HTTP.Request) - -Handler for GET /ping endpoint. -Health check endpoint. -""" -function ping(::HTTP.Request) - return HTTP.Response(200, "") -end - -""" - run_server(port=8082) - -Start the echo server on the given port. -""" -function run_server(port=8082) - try - router = HTTP.Router() - HTTP.register!(router, "GET", "/echo", echo_get) - HTTP.register!(router, "POST", "/echo", echo_post) - HTTP.register!(router, "GET", "/stop", stop) - HTTP.register!(router, "GET", "/ping", ping) - - @info("Starting StressTest server on port $port") - server[] = HTTP.serve!(router, "127.0.0.1", port; stream=false) - wait(server[]) - catch ex - @error("Server error", exception=(ex, catch_backtrace())) - end -end - -end # module StressTestServerImpl - -# Start the server when this script is run directly -StressTestServerImpl.run_server() diff --git a/test/testutils.jl b/test/testutils.jl deleted file mode 100644 index 161d790..0000000 --- a/test/testutils.jl +++ /dev/null @@ -1,68 +0,0 @@ -const opts = Base.JLOptions() -const inline_flag = opts.can_inline == 1 ? `` : `--inline=no` -const cov_flag = (opts.code_coverage == 1) ? `--code-coverage=user` : - (opts.code_coverage == 2) ? `--code-coverage=all` : - `` -const startup_flag = `--startup-file=no` - -# can run servers only on linux for now -const run_tests_with_servers = get(ENV, "RUNNER_OS", "") == "Linux" - -# can only run a subset of tests when running on openapi-generator repo -const openapi_generator_env = get(ENV, "OPENAPI_GENERATOR", "false") == "true" - -function run_server(script, flags=``) - use_pkgimages = VERSION >= v"1.9" ? `--pkgimages=no` : `` - srvrcmd = `$(joinpath(Sys.BINDIR, "julia")) $use_pkgimages $startup_flag $cov_flag $inline_flag $script $flags` - srvrcmd = addenv(srvrcmd, - "JULIA_DEPOT_PATH"=>join(DEPOT_PATH, Sys.iswindows() ? ';' : ':'), - "JULIA_LOAD_PATH"=>join(LOAD_PATH, Sys.iswindows() ? ';' : ':'), - ) - iob = IOBuffer() - pipelined_cmd = pipeline(srvrcmd, stdout=iob, stderr=iob) - @info("Launching ", script, srvrcmd) - ret = run(pipelined_cmd, wait=false) - return ret, iob -end - -function wait_server(port) - @info("Waiting for server", port) - is_ok = timedwait(90.0; pollint=2.0) do - try - resp = HTTP.request("GET", "http://127.0.0.1:$port/ping") - return resp.status == 200 - catch - return false - end - end - - timed_out = is_ok === :timed_out - if timed_out - @warn("Timed out waiting for server", port) - else - @info("Server is ready", port) - end - - return !timed_out -end - -function stop_server(port, proc, iob) - @info("Stopping server", port) - - try - HTTP.request("GET", "http://127.0.0.1:$port/stop") - catch - # ignore - end - - try - wait(proc) - @info("Stopped server", port) - catch ex - server_logs = isnothing(iob) ? "" : String(take!(iob)) - @warn("Error waiting for server", port, server_logs, exception=(ex, catch_backtrace())) - return false - end - - return true -end diff --git a/test/trim/Project.toml b/test/trim/Project.toml new file mode 100644 index 0000000..9286f7a --- /dev/null +++ b/test/trim/Project.toml @@ -0,0 +1,5 @@ +[deps] +HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +JuliaC = "acedd4c2-ced6-4a15-accc-2607eb759ba2" +OpenAPI = "d5e62ea6-ddf3-4d43-8e4c-ad5e6c8bfd7d" diff --git a/test/trim/fixture.jl b/test/trim/fixture.jl new file mode 100644 index 0000000..f73316b --- /dev/null +++ b/test/trim/fixture.jl @@ -0,0 +1,116 @@ +const TRIM_OPENAPI_JSON = raw""" +{ + "openapi": "3.1.0", + "info": {"title": "Trim API", "version": "1.0.0"}, + "servers": [{"url": "https://api.example.test/v1"}], + "components": { + "securitySchemes": { + "BearerAuth": {"type": "http", "scheme": "bearer"} + }, + "schemas": { + "Widget": { + "type": "object", + "required": ["id", "name", "status"], + "properties": { + "id": {"type": "integer", "format": "int64"}, + "name": {"type": "string"}, + "status": {"type": "string", "enum": ["active", "disabled"]}, + "note": {"type": ["string", "null"]}, + "tags": {"type": "array", "items": {"type": "string"}} + }, + "additionalProperties": false + }, + "CreateWidget": { + "type": "object", + "required": ["name"], + "properties": { + "name": {"type": "string"}, + "tags": {"type": "array", "items": {"type": "string"}} + }, + "additionalProperties": false + }, + "Error": { + "type": "object", + "required": ["message"], + "properties": {"message": {"type": "string"}} + } + } + }, + "paths": { + "/widgets/{id}": { + "get": { + "operationId": "getWidget", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": {"type": "integer", "format": "int64"} + }, + { + "name": "verbose", + "in": "query", + "schema": {"type": "boolean"} + } + ], + "responses": { + "200": { + "description": "A widget", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Widget"} + } + } + }, + "default": { + "description": "An error", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Error"} + } + } + } + } + } + }, + "/widgets": { + "post": { + "operationId": "createWidget", + "security": [{"BearerAuth": []}], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/CreateWidget"} + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Widget"} + } + } + } + } + } + }, + "/secret": { + "get": { + "operationId": "getSecret", + "security": [{"BearerAuth": []}], + "responses": { + "200": { + "description": "Secret text", + "content": { + "text/plain": {"schema": {"type": "string"}} + } + } + } + } + } + } +} +""" diff --git a/test/trim_compile_tests.jl b/test/trim_compile_tests.jl new file mode 100644 index 0000000..0e5496e --- /dev/null +++ b/test/trim_compile_tests.jl @@ -0,0 +1,169 @@ +using Test +import Pkg + +include(joinpath(@__DIR__, "trim", "fixture.jl")) + +const _OPENAPI_TRIM_SUPPORTED = VERSION >= v"1.12.0-rc1" +const _OPENAPI_JULIAC_ENTRYPOINT = + "using JuliaC; if isdefined(JuliaC, :main); JuliaC.main(ARGS); else JuliaC._main_cli(ARGS); end" +const _OPENAPI_TRIM_COMPILE_TIMEOUT_S = 600.0 +const _OPENAPI_TRIM_RUN_TIMEOUT_S = 60.0 + +_openapi_package_root(package) = normpath(joinpath(dirname(pathof(package)), "..")) + +function _prepare_openapi_trim_project(trim_project::String)::Nothing + mkpath(trim_project) + cp(joinpath(@__DIR__, "trim", "Project.toml"), joinpath(trim_project, "Project.toml")) + package_specs = Pkg.PackageSpec[ + Pkg.PackageSpec(path = _openapi_package_root(OpenAPI)), + Pkg.PackageSpec(path = _openapi_package_root(OpenAPI.JSON)), + ] + original_project = Base.active_project() + try + Pkg.activate(trim_project) + Pkg.develop(package_specs) + Pkg.instantiate() + finally + original_project === nothing || Pkg.activate(dirname(original_project)) + end + + # Generate from the full fixture before JuliaC runs. This keeps the trim + # workload tied to the current generator instead of a checked-in snapshot. + OpenAPI.client( + TRIM_OPENAPI_JSON; + name = "TrimClient", + path = joinpath(trim_project, "TrimClient.jl"), + ) + cp( + joinpath(@__DIR__, "openapi_trim_workload.jl"), + joinpath(trim_project, "openapi_trim_workload.jl"), + ) + return nothing +end + +function _run_openapi_command(cmd::Cmd; timeout_s::Float64, label::String) + output_path = tempname() + output_stream = open(output_path, "w") + exit_code = -1 + timed_out = false + try + process = run( + pipeline(ignorestatus(cmd), stdout = output_stream, stderr = output_stream); + wait = false, + ) + started = time() + next_update = started + 10.0 + while Base.process_running(process) + now = time() + if now - started >= timeout_s + try + kill(process) + catch + end + timed_out = true + break + end + if now >= next_update + println("[trim] $label WAIT $(round(now - started; digits = 1))s") + flush(stdout) + next_update = now + 10.0 + end + sleep(0.1) + end + try + wait(process) + catch + end + exit_code = something(process.exitcode, -1) + finally + close(output_stream) + end + output = try + read(output_path, String) + finally + rm(output_path; force = true) + end + return exit_code, output, timed_out +end + +function _openapi_trim_totals(output::String) + summary = match( + r"Trim verify finished with\s+(\d+)\s+errors,\s+(\d+)\s+warnings\.", + output, + ) + if summary === nothing + errors = length(collect(eachmatch(r"Verifier error #\d+:", output))) + warnings = length(collect(eachmatch(r"Verifier warning #\d+:", output))) + return errors, warnings + end + return parse(Int, summary.captures[1]), parse(Int, summary.captures[2]) +end + +function _run_openapi_trim_case(trim_project::String)::Nothing + julia = joinpath(Sys.BINDIR, Base.julia_exename()) + script = joinpath(trim_project, "openapi_trim_workload.jl") + println("[trim] compile START openapi_trim_workload.jl") + started = time() + mktempdir() do output_dir + output_name = "openapi_trim_workload" + compile = `$julia --startup-file=no --history-file=no --code-coverage=none --project=$trim_project -e $(_OPENAPI_JULIAC_ENTRYPOINT) -- --output-exe $output_name --project=$trim_project --experimental --trim=safe $script` + exit_code, output, timed_out = cd(output_dir) do + _run_openapi_command( + compile; + timeout_s = _OPENAPI_TRIM_COMPILE_TIMEOUT_S, + label = "compile", + ) + end + timed_out && error("JuliaC trim compile timed out\n$output") + errors, warnings = _openapi_trim_totals(output) + if exit_code != 0 || errors != 0 || warnings != 0 + println("---- trim compile output ----") + println(output) + println("---- end trim compile output ----") + end + @test errors == 0 + @test warnings == 0 + @test exit_code == 0 + + executable = joinpath( + output_dir, + Sys.iswindows() ? output_name * ".exe" : output_name, + ) + @test isfile(executable) + run_exit, run_output, run_timed_out = _run_openapi_command( + `$(abspath(executable))`; + timeout_s = _OPENAPI_TRIM_RUN_TIMEOUT_S, + label = "run", + ) + if run_timed_out || run_exit != 0 + println("---- trim executable output ----") + println(run_output) + println("---- end trim executable output ----") + end + @test !run_timed_out + @test run_exit == 0 + end + println( + "[trim] compile DONE openapi_trim_workload.jl ($(round(time() - started; digits = 2))s)", + ) + return nothing +end + +@testset "JuliaC trim compile" begin + if Sys.iswindows() + println("[trim] skip Windows: JuliaC trim compilation is not stable in package CI") + @test true + elseif Sys.WORD_SIZE != 64 + println("[trim] skip 32-bit Julia: JuliaC trim compilation is covered on 64-bit jobs") + @test true + elseif !_OPENAPI_TRIM_SUPPORTED + println("[trim] skip Julia < 1.12: JuliaC trim compilation is unavailable") + @test true + else + mktempdir() do directory + trim_project = joinpath(directory, "trim_project") + _prepare_openapi_trim_project(trim_project) + _run_openapi_trim_case(trim_project) + end + end +end From 78ec6e433ebafe84bf07663e0386343a23ab917a Mon Sep 17 00:00:00 2001 From: Jacob Quinn Date: Thu, 6 Aug 2026 15:06:52 -0600 Subject: [PATCH 2/9] feat(server): generate server stubs from OpenAPI documents Add OpenAPI.serverplan and OpenAPI.server(source; framework, name, path), mirroring the plan/client pipeline. Split the generated runtime into a direction-agnostic common segment plus client and server segments; the server segment adds the inverse codecs (path/query/cookie style decoders, form-urlencoded and multipart/form-data request readers, and a descriptor-driven response encoder) with request-direction schema validation and structured 400/415 error responses. Framework glue is dispatched through the new OpenAPI.server_source extension seam: OpenAPIHTTPExt emits HTTP.Router modules whose register!(router, impl; path_prefix, middleware) entry point and handler contract match the shape OpenAPI.jl 0.2.x julia-server users implement stubs against (register alias included). Server planning rejects what cannot be decoded faithfully: non-form-data multipart request bodies and operations with more than one exploded object query or cookie parameter. Parameter descriptors gain a shape field and media descriptors a fields element so single-valued exploded arrays decode as arrays; header scalar error messages are direction-neutral now that both directions share them. Co-Authored-By: Claude Fable 5 --- AGENTS.md | 29 +- README.md | 72 +++ ext/OpenAPIHTTPExt.jl | 105 ++++- src/OpenAPI.jl | 7 + src/client.jl | 1014 +++++++++++++++++++++-------------------- src/planning.jl | 136 +++++- src/servergen.jl | 867 +++++++++++++++++++++++++++++++++++ test/runtests.jl | 1 + test/servergen.jl | 614 +++++++++++++++++++++++++ 9 files changed, 2322 insertions(+), 523 deletions(-) create mode 100644 src/servergen.jl create mode 100644 test/servergen.jl diff --git a/AGENTS.md b/AGENTS.md index eb6d810..85f9981 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,14 +2,19 @@ ## Purpose -OpenAPI.jl has two related surfaces: +OpenAPI.jl has three related surfaces: 1. It reads OpenAPI 3.0, 3.1, and 3.2 descriptions and generates typed Julia clients. -2. It creates a smaller OpenAPI 3.2 document from declared Julia operations. +2. It generates typed Julia server-stub modules from the same descriptions; + the framework router glue comes from extensions (`OpenAPIHTTPExt` for + `HTTP.Router`, downstream packages such as Servo.jl for their own routers) + through the `server_source` seam. +3. It creates a smaller OpenAPI 3.2 document from declared Julia operations. -The client generator must fail before code emission when it cannot preserve the -specified wire behavior. Do not generate a plausible but incorrect client. +The generators must fail before code emission when they cannot preserve the +specified wire behavior. Do not generate a plausible but incorrect client or +server. ## Architecture @@ -59,7 +64,9 @@ The main files are: - Treat `src/schema_engine` as provisional. Preserve its extraction boundary so the code can move to JSONSchema.jl after it hardens. - Do not add a dependency on Servo or another server framework. Downstream - packages own router integration through package extensions. + packages own their router integration through package extensions built on + the `server_source`, `register!`, and `operations` seams; only HTTP.jl glue + lives in this repository (in `OpenAPIHTTPExt`). - Put generic strict JSON parsing behavior in JSON.jl when it belongs there. - Treat the normative OpenAPI text as authoritative over published structural schemas. @@ -69,17 +76,21 @@ The main files are: affect names or emitted source. - Do not silently approximate unsupported wire behavior. The known deliberate planning failures are OAS 3.2 `querystring` parameters and streaming or - positional `itemSchema`, `itemEncoding`, and `prefixEncoding` behavior. + positional `itemSchema`, `itemEncoding`, and `prefixEncoding` behavior; the + server planner additionally rejects non-form-data `multipart/*` request + bodies and operations with more than one exploded object query or cookie + parameter. ## Public types and functions - `SourceDocument`, `Diagnostic`, `OpenAPIError`: loading and diagnostics. - `NormalizedAPI`: immutable normalized document. -- `ClientPlan`: deterministic generation plan. +- `ClientPlan`, `ServerPlan`: deterministic generation plans. - `load`, `check`, `normalize`, `plan`, `client`: client pipeline. +- `serverplan`, `server`, `server_module_source`: server-stub generation. - `Param`, `Operation`, `document`: declaration-based authoring API. -- `register!`, `operations`: extension seams implemented by downstream server - packages. +- `register!`, `operations`, `server_source`: extension seams implemented by + the HTTP extension and downstream server packages. Generated modules have their own public runtime types. Important types include `Client`, credential types, `Upload`, `MultipartPartHeaders`, `ApiResponse`, diff --git a/README.md b/README.md index ed97712..db9ac50 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,78 @@ headers, decoded documented headers, and typed body. A non-2xx response throws `ApiError`. The error keeps the raw body even when documented error decoding fails. +## Generate a server + +The same document generates a server-stub module. The document stays the +source of truth: generate the client and the server from one specification and +implement one handler function per operation. + +```julia +using OpenAPI, HTTP + +OpenAPI.server( + "https://example.com/openapi.yaml"; + framework = :HTTP, + name = "ExampleServer", + path = "ExampleServer.jl", +) +``` + +`framework = :HTTP` (the default, available when HTTP.jl is loaded) targets +`HTTP.Router`. Server framework packages add their own emitters through the +`OpenAPI.server_source` extension seam — loading Servo.jl enables +`framework = :Servo`. `OpenAPI.serverplan` is the staged sibling of +`OpenAPI.plan` and rejects documents whose requests cannot be decoded +faithfully (for example `multipart/mixed` request bodies, or two exploded +object query parameters whose wire names cannot be told apart). + +The generated module header lists every handler signature the implementation +must define. Handler functions receive the raw request first, then typed path +parameters in template order, then a required body; optional parameters arrive +as keyword arguments only when the request supplied them. + +```julia +include("ExampleServer.jl") + +module Handlers + +using HTTP + +# GET /widgets/{id} -> get_widget(request, id::Int64; verbose = ABSENT) +function get_widget(request, id; verbose = false) + return lookup_widget(id; verbose) # encoded, validated, 200 +end + +# DELETE /widgets/{id} -> nothing becomes a 204 +delete_widget(request, id) = nothing + +# Return an HTTP.Response directly for anything custom. +create_widget(request, body) = HTTP.Response(409, "already exists") + +end + +router = HTTP.Router() +ExampleServer.register!(router, Handlers; path_prefix = "/v1") +server = HTTP.serve!(router, "127.0.0.1", 8080) +``` + +`register!(router, impl; path_prefix, middleware)` mounts every documented +operation and fails eagerly, listing the expected signatures, when `impl` is +missing any handler. `middleware` wraps each operation handler +(`middleware(handler) -> handler`). `register` is kept as an alias, and the +handler contract — implementation module second, typed positional parameters, +typed-value-or-`HTTP.Response` returns — matches the shape OpenAPI.jl 0.2.x +users generated with `-g julia-server`. + +Request decoding mirrors client encoding: parameter styles (`simple`, `label`, +`matrix`, `form`, `spaceDelimited`, `pipeDelimited`, `deepObject`), header and +cookie parameters, JSON, `application/x-www-form-urlencoded`, and +`multipart/form-data` request bodies, with request-direction schema validation +before handlers run. Decoding failures produce structured JSON `400` (or `415` +for undocumented media types) responses without invoking the handler. Response +values are validated against the output-direction schema and encoded from the +first documented success response. + ## Pipeline and diagnostics The public stages are separate so applications can inspect or cache them. diff --git a/ext/OpenAPIHTTPExt.jl b/ext/OpenAPIHTTPExt.jl index 9c87013..14f6d8b 100644 --- a/ext/OpenAPIHTTPExt.jl +++ b/ext/OpenAPIHTTPExt.jl @@ -1,5 +1,7 @@ # Lets OpenAPI.read fetch documents over HTTP (e.g. a running app's -# /openapi.json). Loads automatically when both OpenAPI and HTTP are loaded. +# /openapi.json), and adds the `framework = :HTTP` server-stub emitter that +# mounts generated operations on an `HTTP.Router`. Loads automatically when +# both OpenAPI and HTTP are loaded. module OpenAPIHTTPExt using OpenAPI, HTTP @@ -57,4 +59,105 @@ OpenAPI.fetchurl(url::AbstractString) = String( ), ) +# ── server-stub emission for HTTP.Router ───────────────────────────────────── + +const GENERATED_HTTP_SERVER_IMPORTS = "using HTTP, JSON, OpenAPI, Base64, Dates, UUIDs" + +const GENERATED_HTTP_SERVER_GLUE = raw""" +function _missing_implementations(impl) + missing_ops = String[] + for entry in _SERVER_OPS + isdefined(impl, entry.invoke) || push!(missing_ops, entry.signature) + end + return missing_ops +end + +function _http_query(request::HTTP.Request) + target = String(request.target) + index = findfirst('?', target) + return index === nothing ? "" : String(SubString(target, index + 1)) +end + +function _http_multipart_parts(content_type, bytes) + startswith(_base_media_type(content_type), "multipart/") || return nothing + parts = HTTP.parse_multipart_form(String(content_type), bytes) + parts === nothing && return nothing + return [ + ( + name = String(part.name), + filename = part.filename, + content_type = String(part.contenttype), + data = read(part.data), + ) for part in parts + ] +end + +function _http_handler(impl, entry) + return function (request::HTTP.Request) + content_values = _header_values(request.headers, "Content-Type") + content_type = isempty(content_values) ? "" : first(content_values) + bytes = Vector{UInt8}(codeunits(String(request.body))) + local args, kwargs + try + parts = _http_multipart_parts(content_type, bytes) + args, kwargs = _operation_arguments( + entry, + something(HTTP.getparams(request), Dict{String,String}()), + _http_query(request), + request.headers, + bytes, + parts, + ) + catch error + status, headers, payload = _request_error_response(error) + return HTTP.Response(status, headers, payload) + end + result = getfield(impl, entry.invoke)(request, args...; kwargs...) + result isa HTTP.Response && return result + status, headers, payload = try + _server_response(entry.operation, result) + catch error + _response_error_payload(error) + end + return HTTP.Response(status, headers, payload) + end +end + +# register!(router::HTTP.Router, impl; path_prefix = "", middleware = nothing) +# +# Mount every documented operation on `router`, dispatching to the handler +# functions `impl` defines (one per operation; the expected signatures are +# listed at the top of this file). Handlers may return a documented typed +# value (encoded and validated automatically), `nothing` (a 204 response), or +# a full `HTTP.Response` for anything custom. `middleware` wraps each +# operation handler: `middleware(handler) -> handler`. `register` is an alias +# kept for familiarity with OpenAPI.jl 0.2.x generated servers. +function register!( + router::HTTP.Router, + impl; + path_prefix::AbstractString = "", + middleware = nothing, +) + missing_ops = _missing_implementations(impl) + isempty(missing_ops) || throw(ArgumentError(string( + "implementation is missing handler functions:\n ", + join(missing_ops, "\n "), + ))) + for entry in _SERVER_OPS + handler = _http_handler(impl, entry) + middleware === nothing || (handler = middleware(handler)) + HTTP.register!(router, entry.method, string(path_prefix, entry.path), handler) + end + return router +end +const register = register! +""" + +OpenAPI.server_source(::Val{:HTTP}, plan::OpenAPI.ServerPlan) = + OpenAPI.server_module_source( + plan; + imports = GENERATED_HTTP_SERVER_IMPORTS, + glue = GENERATED_HTTP_SERVER_GLUE, + ) + end # module diff --git a/src/OpenAPI.jl b/src/OpenAPI.jl index 4cb17eb..9f882c7 100644 --- a/src/OpenAPI.jl +++ b/src/OpenAPI.jl @@ -12,6 +12,12 @@ Two pieces: running app) into a deterministic single-file Julia client. Generated modules use HTTP.jl for transport and JSON.jl plus OpenAPI's provisional schema engine for typed, validated request and response handling. +3. **Server generation** — [`OpenAPI.server`](@ref) turns the same documents + into a deterministic single-file server-stub module: typed request decoding, + response validation and encoding, and a `register!(router, impl)` entry + point that mounts handler functions you implement onto a framework router + (`HTTP.Router` through the HTTP extension; other frameworks through the + [`OpenAPI.server_source`](@ref) seam). """ module OpenAPI @@ -35,6 +41,7 @@ include("planning.jl") include("read.jl") include("runtime.jl") include("client.jl") +include("servergen.jl") # ── extension seams ───────────────────────────────────────────────────────── diff --git a/src/client.jl b/src/client.jl index b8d27c7..e9377bc 100644 --- a/src/client.jl +++ b/src/client.jl @@ -1,4 +1,4 @@ -const GENERATED_RUNTIME = raw""" +const GENERATED_RUNTIME_COMMON = raw""" struct Absent end const ABSENT = Absent() Base.show(io::IO, ::Absent) = print(io, "ABSENT") @@ -8,58 +8,6 @@ struct DecodeError <: Exception end Base.showerror(io::IO, error::DecodeError) = print(io, error.message) -struct ApiError <: Exception - operation_id::String - status::Int - headers::Vector{Pair{String,String}} - decoded_headers::Dict{String,Any} - body::Vector{UInt8} - decoded::Any - decode_error::Any -end -function Base.showerror(io::IO, error::ApiError) - print(io, "ApiError(", error.status, ") in ", error.operation_id) - if error.decode_error !== nothing - print(io, " (response decoding failed: ") - showerror(io, error.decode_error) - print(io, ')') - end - isempty(error.body) && return - text = isvalid(String, error.body) ? String(copy(error.body)) : nothing - if text === nothing - preview = bytes2hex(error.body[1:min(length(error.body), 32)]) - print(io, ": ", length(error.body), " binary bytes (", preview) - length(error.body) > 32 && print(io, "…") - print(io, ')') - else - preview = first(text, min(length(text), 512)) - print(io, ": ", preview) - length(text) > 512 && print(io, "…") - end -end - -struct ApiResponse{T} - status::Int - headers::Vector{Pair{String,String}} - decoded_headers::Dict{String,Any} - body::T -end - -struct UnexpectedBody <: Exception - operation_id::String - status::Int - bytes::Int -end -Base.showerror(io::IO, error::UnexpectedBody) = print( - io, - "operation ", - error.operation_id, - " received an undocumented ", - error.bytes, - "-byte response body for status ", - error.status, -) - struct UnsupportedMediaType <: Exception media_type::String direction::Symbol @@ -72,26 +20,6 @@ Base.showerror(io::IO, error::UnsupportedMediaType) = print( repr(error.media_type), ) -struct UnexpectedContentType <: Exception - operation_id::String - status::Int - received::String - expected::Tuple -end -function Base.showerror(io::IO, error::UnexpectedContentType) - print( - io, - "unexpected Content-Type ", - repr(error.received), - " for ", - error.operation_id, - " response ", - error.status, - "; expected ", - join(error.expected, ", "), - ) -end - struct SchemaValidationError <: Exception context::String issues::Vector{Any} @@ -217,43 +145,6 @@ end _schema_valid(descriptor, value; direction::Symbol = :neutral) = isempty(_schema_issues(descriptor, value; direction)) -abstract type AbstractCredential end -struct ApiKeyCredential <: AbstractCredential - value::String - authorizations::Set{String} -end -ApiKeyCredential(value::AbstractString; roles = String[]) = - ApiKeyCredential(String(value), Set(String.(roles))) -struct BasicCredential <: AbstractCredential - username::String - password::String - authorizations::Set{String} -end -BasicCredential(username::AbstractString, password::AbstractString; roles = String[]) = - BasicCredential(String(username), String(password), Set(String.(roles))) -struct BearerCredential <: AbstractCredential - token::String - authorizations::Set{String} -end -function BearerCredential(token::AbstractString; scopes = String[], roles = String[]) - return BearerCredential( - String(token), - union(Set(String.(scopes)), Set(String.(roles))), - ) -end -struct HttpCredential <: AbstractCredential - value::String - authorizations::Set{String} -end -HttpCredential(value::AbstractString; roles = String[]) = - HttpCredential(String(value), Set(String.(roles))) -struct MutualTLSCredential <: AbstractCredential - request_options::NamedTuple - authorizations::Set{String} -end -MutualTLSCredential(request_options::NamedTuple; roles = String[]) = - MutualTLSCredential(request_options, Set(String.(roles))) - struct Upload data::Vector{UInt8} filename::Union{Nothing,String} @@ -282,151 +173,6 @@ function Upload( ) end -mutable struct Client - server::Union{Nothing,String} - server_index::Int - server_name::Union{Nothing,String} - server_variables::Dict{String,String} - credentials::Dict{String,AbstractCredential} - headers::Vector{Pair{String,String}} - request_options::NamedTuple - require_credentials::Bool - media_encoders::Dict{String,Function} - media_decoders::Dict{String,Function} - validate_requests::Bool - validate_responses::Bool -end - -function _normalize_media_codecs(codecs::Dict{String,Function}) - normalized = Dict{String,Function}() - for name in keys(codecs) - normalized[_base_media_type(name)] = codecs[name] - end - return normalized -end - -function _normalize_media_codecs(codecs::AbstractDict) - return Dict{String,Function}( - _base_media_type(name) => codec for (name, codec) in codecs - ) -end - -function Client( - server::Union{Nothing,AbstractString} = nothing; - server_index::Integer = 1, - server_name::Union{Nothing,AbstractString} = nothing, - server_variables::AbstractDict = Dict{String,String}(), - credentials::AbstractDict = Dict{String,AbstractCredential}(), - headers = Pair{String,String}[], - request_options::NamedTuple = NamedTuple(), - require_credentials::Bool = true, - media_encoders::AbstractDict = Dict{String,Function}(), - media_decoders::AbstractDict = Dict{String,Function}(), - validate_requests::Bool = true, - validate_responses::Bool = true, -) - server_index > 0 || throw(ArgumentError("server_index must be positive")) - normalized_credentials = credentials isa Dict{String,AbstractCredential} ? - copy(credentials) : - Dict{String,AbstractCredential}( - String(name) => credential for (name, credential) in credentials - ) - normalized_headers = headers isa Vector{Pair{String,String}} ? copy(headers) : - Pair{String,String}[ - String(name) => String(value) for (name, value) in headers - ] - return Client( - server === nothing ? nothing : String(rstrip(server, '/')), - Int(server_index), - server_name === nothing ? nothing : String(server_name), - server_variables isa Dict{String,String} ? copy(server_variables) : - Dict{String,String}( - String(name) => String(value) for (name, value) in server_variables - ), - normalized_credentials, - normalized_headers, - request_options, - require_credentials, - _normalize_media_codecs(media_encoders), - _normalize_media_codecs(media_decoders), - validate_requests, - validate_responses, - ) -end - -function credential!(client::Client, name::AbstractString, credential::AbstractCredential) - client.credentials[String(name)] = credential - return client -end -credential!(name::AbstractString, credential::AbstractCredential) = - credential!(DEFAULT_CLIENT, name, credential) - -function clearcredential!(client::Client, name::AbstractString) - delete!(client.credentials, String(name)) - return client -end -clearcredential!(name::AbstractString) = clearcredential!(DEFAULT_CLIENT, name) - -function server!(client::Client, server::Union{Nothing,AbstractString}) - client.server = server === nothing ? nothing : String(rstrip(server, '/')) - isdefined(@__MODULE__, :DEFAULT_CLIENT) && client === DEFAULT_CLIENT && - (SERVER[] = something(client.server, _DEFAULT_SERVER)) - return client -end -server!(server::Union{Nothing,AbstractString}) = server!(DEFAULT_CLIENT, server) - -function server_index!(client::Client, index::Integer) - index > 0 || throw(ArgumentError("server index must be positive")) - client.server_index = Int(index) - client.server_name = nothing - return client -end -server_index!(index::Integer) = server_index!(DEFAULT_CLIENT, index) - -function server_name!(client::Client, name::Union{Nothing,AbstractString}) - client.server_name = name === nothing ? nothing : String(name) - return client -end -server_name!(name::Union{Nothing,AbstractString}) = server_name!(DEFAULT_CLIENT, name) - -function server_variable!(client::Client, name::AbstractString, value::AbstractString) - client.server_variables[String(name)] = String(value) - return client -end -server_variable!(name::AbstractString, value::AbstractString) = - server_variable!(DEFAULT_CLIENT, name, value) - -function codec!( - client::Client, - media_type::AbstractString; - encode::Union{Nothing,Function} = nothing, - decode::Union{Nothing,Function} = nothing, -) - key = _base_media_type(media_type) - encode === nothing || (client.media_encoders[key] = encode) - decode === nothing || (client.media_decoders[key] = decode) - return client -end -codec!(media_type::AbstractString; kwargs...) = - codec!(DEFAULT_CLIENT, media_type; kwargs...) - -function authorization!(client::Client, token::Union{Nothing,AbstractString}) - names = String[ - name for (name, scheme) in _SECURITY_SCHEMES - if scheme.type in (:http_bearer, :oauth2, :openidconnect) - ] - isempty(names) && throw(ArgumentError("the OpenAPI document has no bearer-compatible security scheme")) - for name in names - if token === nothing - delete!(client.credentials, name) - else - client.credentials[name] = BearerCredential(String(token)) - end - end - return client -end -authorization!(token::Union{Nothing,AbstractString}) = authorization!(DEFAULT_CLIENT, token) - _typename(::Type{T}) where {T} = string(T) _required(value, key, model) = haskey(value, key) ? value[key] : throw(DecodeError("required field $(repr(key)) is missing while decoding $model")) @@ -593,14 +339,493 @@ function _encode(value::AbstractDict) end return output end -function _encode(value::NamedTuple) - output = JSON.Object{String,Any}() - for (key, item) in pairs(value) - output[String(key)] = _encode(item) +function _encode(value::NamedTuple) + output = JSON.Object{String,Any}() + for (key, item) in pairs(value) + output[String(key)] = _encode(item) + end + return output +end +_encode(value) = value + +function _safe_header(name, value) + header = String(name) + content = String(value) + occursin(r"^[!#$%&'*+.^_`|~0-9A-Za-z-]+$", header) || + throw(ArgumentError("invalid HTTP header name $(repr(header))")) + (occursin('\r', content) || occursin('\n', content)) && + throw(ArgumentError("HTTP header values must not contain CR or LF")) + return header => content +end + +function _set_header!(headers, name, value) + pair = _safe_header(name, value) + lowered = lowercase(pair.first) + filter!(entry -> lowercase(entry.first) != lowered, headers) + push!(headers, pair) + return headers +end + +function _media_match_score(received::String, documented::String) + received == documented && return 4 + documented == "*/*" && return 1 + parts = split(documented, '/'; limit = 2) + received_parts = split(received, '/'; limit = 2) + length(parts) == 2 && length(received_parts) == 2 || return 0 + parts[1] == received_parts[1] || parts[1] == "*" || return 0 + parts[2] == "*" && return parts[1] == "*" ? 1 : 2 + startswith(parts[2], "*+") && + endswith(received_parts[2], parts[2][2:end]) && return 3 + return 0 +end + +_media_match(received::String, documented::String) = + _media_match_score(_base_media_type(received), _base_media_type(documented)) > 0 + +_base_media_type(value) = lowercase(strip(first(split(String(value), ';'; limit = 2)))) + +function _select_media(media, received) + isempty(media) && return nothing + normalized = _base_media_type(received) + selected = nothing + score = 0 + for entry in media + candidate = _media_match_score(normalized, _base_media_type(entry[1])) + if candidate > score + selected = entry + score = candidate + end + end + return selected +end + +function _select_response(responses, status) + exact = string(status) + range = string(div(status, 100), "XX") + default = nothing + for response in responses + selector = uppercase(response.selector) + selector == exact && return response + selector == range && (default = response) + selector == "DEFAULT" && default === nothing && (default = response) + end + return default +end + +function _header_values(headers, name) + lowered = lowercase(String(name)) + return String[ + value for (key, value) in headers if lowercase(String(key)) == lowered + ] +end + +function _header_atom(value) + text = String(value) + lowered = lowercase(text) + lowered == "true" && return true + lowered == "false" && return false + lowered == "null" && return nothing + integer = tryparse(Int64, text) + integer === nothing || return integer + number = tryparse(Float64, text) + number === nothing || return number + return text +end + +function _header_scalar(type, value) + text = String(value) + type === String && return text + type === Bool && return lowercase(text) == "true" ? true : + lowercase(text) == "false" ? false : + throw(DecodeError("invalid boolean value $(repr(text))")) + type <: Integer && return try + parse(type, text) + catch error + throw(DecodeError("invalid integer value: $(sprint(showerror, error))")) + end + type <: AbstractFloat && return try + parse(type, text) + catch error + throw(DecodeError("invalid numeric value: $(sprint(showerror, error))")) + end + return _decode(type, text) +end + +function _header_type_variant(type, shape) + variants = type isa Union ? Base.uniontypes(type) : (type,) + for variant in variants + variant in (Nothing, Absent) && continue + shape === :array && variant <: AbstractVector && return variant + shape === :object && + (variant <: AbstractDict || isstructtype(variant)) && return variant + shape === :scalar && return variant + end + return type +end + +function _decode_schema_header( + type, + values, + shape, + explode, + schema; + set_cookie::Bool = false, + direction::Symbol = :output, + context = "decoding a response header", +) + selected = _header_type_variant(type, shape) + raw = if shape === :array + items = if set_cookie + String[String(value) for value in values] + else + output = String[] + for value in values + append!(output, split(value, ',')) + end + output + end + element = selected <: AbstractVector ? eltype(selected) : String + Any[_header_scalar(element, item) for item in items] + elseif shape === :object + object = JSON.Object{String,Any}() + if set_cookie && explode + for value in values + pair = split(value, '='; limit = 2) + length(pair) == 2 || throw( + DecodeError("invalid Set-Cookie response header"), + ) + object[pair[1]] = pair[2] + end + elseif explode + tokens = split(join(values, ','), ',') + for token in tokens + pair = split(token, '='; limit = 2) + length(pair) == 2 || throw( + DecodeError("invalid exploded object response header"), + ) + object[pair[1]] = _header_atom(pair[2]) + end + else + tokens = split(join(values, ','), ',') + iseven(length(tokens)) || + throw(DecodeError("invalid object response header")) + for index in 1:2:length(tokens) + object[tokens[index]] = _header_atom(tokens[index + 1]) + end + end + object + else + _header_scalar(selected, join(values, set_cookie ? '\n' : ',')) + end + _validate_schema( + schema, + _encode(raw), + context; + direction, + ) + return _decode(type, raw) +end + +_is_json_media(media) = media == "application/json" || endswith(media, "+json") +_is_sequential_json_media(media) = media in ( + "application/jsonl", + "application/x-ndjson", + "application/json-seq", + "application/geo+json-seq", +) || endswith(media, "+json-seq") + +function _parse_json(text, context) + source = text isa AbstractString ? String(text) : String(copy(text)) + isvalid(source) || throw(DecodeError("invalid UTF-8 while $context")) + try + return JSON.parse(source; duplicate_keys = :error) + catch error + throw(DecodeError("invalid JSON while $context: $(sprint(showerror, error))")) + end +end + +function _decode_sequential_json(body, media) + records = Any[] + if media == "application/json-seq" || endswith(media, "+json-seq") + for chunk in split(String(body), Char(0x1e)) + text = strip(chunk) + isempty(text) || push!(records, _parse_json(text, "decoding a JSON sequence")) + end + else + for line in eachline(IOBuffer(body)) + text = strip(line) + isempty(text) || push!(records, _parse_json(text, "decoding JSON lines")) + end + end + return records +end + +function _encode_sequential_json(value, media) + encoded = _encode(value) + encoded isa AbstractVector || encoded isa Tuple || throw( + ArgumentError("sequential JSON request bodies must be arrays or tuples"), + ) + if media == "application/json-seq" || endswith(media, "+json-seq") + return join((string(Char(0x1e), JSON.json(item), '\n') for item in encoded)) + end + return join((JSON.json(item) * "\n" for item in encoded)) +end +""" + +const GENERATED_RUNTIME = raw""" +struct ApiError <: Exception + operation_id::String + status::Int + headers::Vector{Pair{String,String}} + decoded_headers::Dict{String,Any} + body::Vector{UInt8} + decoded::Any + decode_error::Any +end +function Base.showerror(io::IO, error::ApiError) + print(io, "ApiError(", error.status, ") in ", error.operation_id) + if error.decode_error !== nothing + print(io, " (response decoding failed: ") + showerror(io, error.decode_error) + print(io, ')') + end + isempty(error.body) && return + text = isvalid(String, error.body) ? String(copy(error.body)) : nothing + if text === nothing + preview = bytes2hex(error.body[1:min(length(error.body), 32)]) + print(io, ": ", length(error.body), " binary bytes (", preview) + length(error.body) > 32 && print(io, "…") + print(io, ')') + else + preview = first(text, min(length(text), 512)) + print(io, ": ", preview) + length(text) > 512 && print(io, "…") + end +end + +struct ApiResponse{T} + status::Int + headers::Vector{Pair{String,String}} + decoded_headers::Dict{String,Any} + body::T +end + +struct UnexpectedBody <: Exception + operation_id::String + status::Int + bytes::Int +end +Base.showerror(io::IO, error::UnexpectedBody) = print( + io, + "operation ", + error.operation_id, + " received an undocumented ", + error.bytes, + "-byte response body for status ", + error.status, +) + +struct UnexpectedContentType <: Exception + operation_id::String + status::Int + received::String + expected::Tuple +end +function Base.showerror(io::IO, error::UnexpectedContentType) + print( + io, + "unexpected Content-Type ", + repr(error.received), + " for ", + error.operation_id, + " response ", + error.status, + "; expected ", + join(error.expected, ", "), + ) +end + +abstract type AbstractCredential end +struct ApiKeyCredential <: AbstractCredential + value::String + authorizations::Set{String} +end +ApiKeyCredential(value::AbstractString; roles = String[]) = + ApiKeyCredential(String(value), Set(String.(roles))) +struct BasicCredential <: AbstractCredential + username::String + password::String + authorizations::Set{String} +end +BasicCredential(username::AbstractString, password::AbstractString; roles = String[]) = + BasicCredential(String(username), String(password), Set(String.(roles))) +struct BearerCredential <: AbstractCredential + token::String + authorizations::Set{String} +end +function BearerCredential(token::AbstractString; scopes = String[], roles = String[]) + return BearerCredential( + String(token), + union(Set(String.(scopes)), Set(String.(roles))), + ) +end +struct HttpCredential <: AbstractCredential + value::String + authorizations::Set{String} +end +HttpCredential(value::AbstractString; roles = String[]) = + HttpCredential(String(value), Set(String.(roles))) +struct MutualTLSCredential <: AbstractCredential + request_options::NamedTuple + authorizations::Set{String} +end +MutualTLSCredential(request_options::NamedTuple; roles = String[]) = + MutualTLSCredential(request_options, Set(String.(roles))) + +mutable struct Client + server::Union{Nothing,String} + server_index::Int + server_name::Union{Nothing,String} + server_variables::Dict{String,String} + credentials::Dict{String,AbstractCredential} + headers::Vector{Pair{String,String}} + request_options::NamedTuple + require_credentials::Bool + media_encoders::Dict{String,Function} + media_decoders::Dict{String,Function} + validate_requests::Bool + validate_responses::Bool +end + +function _normalize_media_codecs(codecs::Dict{String,Function}) + normalized = Dict{String,Function}() + for name in keys(codecs) + normalized[_base_media_type(name)] = codecs[name] + end + return normalized +end + +function _normalize_media_codecs(codecs::AbstractDict) + return Dict{String,Function}( + _base_media_type(name) => codec for (name, codec) in codecs + ) +end + +function Client( + server::Union{Nothing,AbstractString} = nothing; + server_index::Integer = 1, + server_name::Union{Nothing,AbstractString} = nothing, + server_variables::AbstractDict = Dict{String,String}(), + credentials::AbstractDict = Dict{String,AbstractCredential}(), + headers = Pair{String,String}[], + request_options::NamedTuple = NamedTuple(), + require_credentials::Bool = true, + media_encoders::AbstractDict = Dict{String,Function}(), + media_decoders::AbstractDict = Dict{String,Function}(), + validate_requests::Bool = true, + validate_responses::Bool = true, +) + server_index > 0 || throw(ArgumentError("server_index must be positive")) + normalized_credentials = credentials isa Dict{String,AbstractCredential} ? + copy(credentials) : + Dict{String,AbstractCredential}( + String(name) => credential for (name, credential) in credentials + ) + normalized_headers = headers isa Vector{Pair{String,String}} ? copy(headers) : + Pair{String,String}[ + String(name) => String(value) for (name, value) in headers + ] + return Client( + server === nothing ? nothing : String(rstrip(server, '/')), + Int(server_index), + server_name === nothing ? nothing : String(server_name), + server_variables isa Dict{String,String} ? copy(server_variables) : + Dict{String,String}( + String(name) => String(value) for (name, value) in server_variables + ), + normalized_credentials, + normalized_headers, + request_options, + require_credentials, + _normalize_media_codecs(media_encoders), + _normalize_media_codecs(media_decoders), + validate_requests, + validate_responses, + ) +end + +function credential!(client::Client, name::AbstractString, credential::AbstractCredential) + client.credentials[String(name)] = credential + return client +end +credential!(name::AbstractString, credential::AbstractCredential) = + credential!(DEFAULT_CLIENT, name, credential) + +function clearcredential!(client::Client, name::AbstractString) + delete!(client.credentials, String(name)) + return client +end +clearcredential!(name::AbstractString) = clearcredential!(DEFAULT_CLIENT, name) + +function server!(client::Client, server::Union{Nothing,AbstractString}) + client.server = server === nothing ? nothing : String(rstrip(server, '/')) + isdefined(@__MODULE__, :DEFAULT_CLIENT) && client === DEFAULT_CLIENT && + (SERVER[] = something(client.server, _DEFAULT_SERVER)) + return client +end +server!(server::Union{Nothing,AbstractString}) = server!(DEFAULT_CLIENT, server) + +function server_index!(client::Client, index::Integer) + index > 0 || throw(ArgumentError("server index must be positive")) + client.server_index = Int(index) + client.server_name = nothing + return client +end +server_index!(index::Integer) = server_index!(DEFAULT_CLIENT, index) + +function server_name!(client::Client, name::Union{Nothing,AbstractString}) + client.server_name = name === nothing ? nothing : String(name) + return client +end +server_name!(name::Union{Nothing,AbstractString}) = server_name!(DEFAULT_CLIENT, name) + +function server_variable!(client::Client, name::AbstractString, value::AbstractString) + client.server_variables[String(name)] = String(value) + return client +end +server_variable!(name::AbstractString, value::AbstractString) = + server_variable!(DEFAULT_CLIENT, name, value) + +function codec!( + client::Client, + media_type::AbstractString; + encode::Union{Nothing,Function} = nothing, + decode::Union{Nothing,Function} = nothing, +) + key = _base_media_type(media_type) + encode === nothing || (client.media_encoders[key] = encode) + decode === nothing || (client.media_decoders[key] = decode) + return client +end +codec!(media_type::AbstractString; kwargs...) = + codec!(DEFAULT_CLIENT, media_type; kwargs...) + +function authorization!(client::Client, token::Union{Nothing,AbstractString}) + names = String[ + name for (name, scheme) in _SECURITY_SCHEMES + if scheme.type in (:http_bearer, :oauth2, :openidconnect) + ] + isempty(names) && throw(ArgumentError("the OpenAPI document has no bearer-compatible security scheme")) + for name in names + if token === nothing + delete!(client.credentials, name) + else + client.credentials[name] = BearerCredential(String(token)) + end end - return output + return client end -_encode(value) = value +authorization!(token::Union{Nothing,AbstractString}) = authorization!(DEFAULT_CLIENT, token) _scalar(::Nothing) = "" _scalar(value::Bool) = value ? "true" : "false" @@ -912,24 +1137,6 @@ function _append_parameter!(client, path, query, headers, cookies, descriptor, v return path end -function _safe_header(name, value) - header = String(name) - content = String(value) - occursin(r"^[!#$%&'*+.^_`|~0-9A-Za-z-]+$", header) || - throw(ArgumentError("invalid HTTP header name $(repr(header))")) - (occursin('\r', content) || occursin('\n', content)) && - throw(ArgumentError("HTTP header values must not contain CR or LF")) - return header => content -end - -function _set_header!(headers, name, value) - pair = _safe_header(name, value) - lowered = lowercase(pair.first) - filter!(entry -> lowercase(entry.first) != lowered, headers) - push!(headers, pair) - return headers -end - _authorizations(credential::AbstractCredential) = credential.authorizations function _credential_satisfies(scheme, credential, required) @@ -1009,164 +1216,6 @@ function _security!(client, requirements, query, headers, cookies, base_options) return base_options end -function _media_match_score(received::String, documented::String) - received == documented && return 4 - documented == "*/*" && return 1 - parts = split(documented, '/'; limit = 2) - received_parts = split(received, '/'; limit = 2) - length(parts) == 2 && length(received_parts) == 2 || return 0 - parts[1] == received_parts[1] || parts[1] == "*" || return 0 - parts[2] == "*" && return parts[1] == "*" ? 1 : 2 - startswith(parts[2], "*+") && - endswith(received_parts[2], parts[2][2:end]) && return 3 - return 0 -end - -_media_match(received::String, documented::String) = - _media_match_score(_base_media_type(received), _base_media_type(documented)) > 0 - -_base_media_type(value) = lowercase(strip(first(split(String(value), ';'; limit = 2)))) - -function _select_media(media, received) - isempty(media) && return nothing - normalized = _base_media_type(received) - selected = nothing - score = 0 - for entry in media - candidate = _media_match_score(normalized, _base_media_type(entry[1])) - if candidate > score - selected = entry - score = candidate - end - end - return selected -end - -function _select_response(responses, status) - exact = string(status) - range = string(div(status, 100), "XX") - default = nothing - for response in responses - selector = uppercase(response.selector) - selector == exact && return response - selector == range && (default = response) - selector == "DEFAULT" && default === nothing && (default = response) - end - return default -end - -function _header_values(headers, name) - lowered = lowercase(String(name)) - return String[ - value for (key, value) in headers if lowercase(String(key)) == lowered - ] -end - -function _header_atom(value) - text = String(value) - lowered = lowercase(text) - lowered == "true" && return true - lowered == "false" && return false - lowered == "null" && return nothing - integer = tryparse(Int64, text) - integer === nothing || return integer - number = tryparse(Float64, text) - number === nothing || return number - return text -end - -function _header_scalar(type, value) - text = String(value) - type === String && return text - type === Bool && return lowercase(text) == "true" ? true : - lowercase(text) == "false" ? false : - throw(DecodeError("invalid boolean response header $(repr(text))")) - type <: Integer && return try - parse(type, text) - catch error - throw(DecodeError("invalid integer response header: $(sprint(showerror, error))")) - end - type <: AbstractFloat && return try - parse(type, text) - catch error - throw(DecodeError("invalid numeric response header: $(sprint(showerror, error))")) - end - return _decode(type, text) -end - -function _header_type_variant(type, shape) - variants = type isa Union ? Base.uniontypes(type) : (type,) - for variant in variants - variant in (Nothing, Absent) && continue - shape === :array && variant <: AbstractVector && return variant - shape === :object && - (variant <: AbstractDict || isstructtype(variant)) && return variant - shape === :scalar && return variant - end - return type -end - -function _decode_schema_header( - type, - values, - shape, - explode, - schema; - set_cookie::Bool = false, -) - selected = _header_type_variant(type, shape) - raw = if shape === :array - items = if set_cookie - String[String(value) for value in values] - else - output = String[] - for value in values - append!(output, split(value, ',')) - end - output - end - element = selected <: AbstractVector ? eltype(selected) : String - Any[_header_scalar(element, item) for item in items] - elseif shape === :object - object = JSON.Object{String,Any}() - if set_cookie && explode - for value in values - pair = split(value, '='; limit = 2) - length(pair) == 2 || throw( - DecodeError("invalid Set-Cookie response header"), - ) - object[pair[1]] = pair[2] - end - elseif explode - tokens = split(join(values, ','), ',') - for token in tokens - pair = split(token, '='; limit = 2) - length(pair) == 2 || throw( - DecodeError("invalid exploded object response header"), - ) - object[pair[1]] = _header_atom(pair[2]) - end - else - tokens = split(join(values, ','), ',') - iseven(length(tokens)) || - throw(DecodeError("invalid object response header")) - for index in 1:2:length(tokens) - object[tokens[index]] = _header_atom(tokens[index + 1]) - end - end - object - else - _header_scalar(selected, join(values, set_cookie ? '\n' : ',')) - end - _validate_schema( - schema, - _encode(raw), - "decoding a response header"; - direction = :output, - ) - return _decode(type, raw) -end - function _decode_response_headers(client, descriptor, headers) output = Dict{String,Any}() descriptor === nothing && return output @@ -1203,40 +1252,6 @@ function _decode_response_headers(client, descriptor, headers) return output end -_is_json_media(media) = media == "application/json" || endswith(media, "+json") -_is_sequential_json_media(media) = media in ( - "application/jsonl", - "application/x-ndjson", - "application/json-seq", - "application/geo+json-seq", -) || endswith(media, "+json-seq") - -function _parse_json(text, context) - source = text isa AbstractString ? String(text) : String(copy(text)) - isvalid(source) || throw(DecodeError("invalid UTF-8 while $context")) - try - return JSON.parse(source; duplicate_keys = :error) - catch error - throw(DecodeError("invalid JSON while $context: $(sprint(showerror, error))")) - end -end - -function _decode_sequential_json(body, media) - records = Any[] - if media == "application/json-seq" || endswith(media, "+json-seq") - for chunk in split(String(body), Char(0x1e)) - text = strip(chunk) - isempty(text) || push!(records, _parse_json(text, "decoding a JSON sequence")) - end - else - for line in eachline(IOBuffer(body)) - text = strip(line) - isempty(text) || push!(records, _parse_json(text, "decoding JSON lines")) - end - end - return records -end - function _decode_body(client::Client, type, content_type, body, schema) media = _base_media_type(content_type) decoder = get(client.media_decoders, media, nothing) @@ -1724,16 +1739,6 @@ function _multipart_body( return take!(io), String(media_type) * "; boundary=" * boundary end -function _encode_sequential_json(value, media) - encoded = _encode(value) - encoded isa AbstractVector || encoded isa Tuple || throw( - ArgumentError("sequential JSON request bodies must be arrays or tuples"), - ) - if media == "application/json-seq" || endswith(media, "+json-seq") - return join((string(Char(0x1e), JSON.json(item), '\n') for item in encoded)) - end - return join((JSON.json(item) * "\n" for item in encoded)) -end function _encode_body( client, @@ -2153,7 +2158,7 @@ function _security_type(scheme::NormalizedSecurityScheme) return scheme.type end -function _emit_security(io::IO, plan::ClientPlan) +function _emit_security(io::IO, plan::GenerationPlan) println(io, "const _SECURITY_SCHEMES = Dict{String,NamedTuple}(") for scheme in sort(collect(plan.api.security_schemes); by = item -> item.name) print(io, " ", repr(scheme.name), " => (") @@ -2165,7 +2170,7 @@ function _emit_security(io::IO, plan::ClientPlan) println(io, ")\n") end -function _model_indices(plan::ClientPlan) +function _model_indices(plan::GenerationPlan) return Dict(model.name => index for (index, model) in enumerate(plan.models)) end @@ -2179,7 +2184,7 @@ function _model_references(type::String, names) return output end -function _cyclic_aliases(plan::ClientPlan) +function _cyclic_aliases(plan::GenerationPlan) aliases = Set(model.name for model in plan.models if model.kind === :alias) edges = Dict( model.name => _model_references(something(model.alias, ""), aliases) for @@ -2210,7 +2215,7 @@ function _model_types(model::ModelPlan, wrapped_aliases) return String[] end -function _forward_abstracts(plan::ClientPlan, wrapped_aliases) +function _forward_abstracts(plan::GenerationPlan, wrapped_aliases) indices = _model_indices(plan) concrete = Set(model.name for model in plan.models if model.kind !== :alias) union!(concrete, wrapped_aliases) @@ -2547,7 +2552,7 @@ function _source_schema_node(registry, node::Resources.NodeId) return Resources.NodeId(source_resource.id, pointer) end -function _directional_required_rules(plan::ClientPlan, template) +function _directional_required_rules(plan::GenerationPlan, template) registry = template.registry directional_cache = Dict{Tuple{Resources.NodeId,String},Bool}() rules = Dict{ @@ -2604,7 +2609,7 @@ function _directional_required_rules(plan::ClientPlan, template) return output end -function _client_schema_handles(plan::ClientPlan) +function _client_schema_handles(plan::GenerationPlan) handles = SchemaHandle[last(pair) for pair in plan.api.schemas] for operation in plan.api.operations operation.direction === :request || continue @@ -2640,7 +2645,7 @@ function _client_schema_handles(plan::ClientPlan) return handles end -function _emit_schema_data(io::IO, plan::ClientPlan) +function _emit_schema_data(io::IO, plan::GenerationPlan) handles = _client_schema_handles(plan) graph = isempty(handles) ? nothing : first(handles).workspace.compiled if graph === nothing @@ -2768,6 +2773,7 @@ function _parameter_descriptor(parameter::ParameterPlan) ", style = " * repr(something(parameter.style, :none)) * ", explode = " * (parameter.explode === true ? "true" : "false") * ", allow_reserved = " * (parameter.allow_reserved ? "true" : "false") * + ", shape = " * repr(_schema_shape(_parameter_schema(parameter.parameter))) * ", schema = " * _schema_descriptor(_schema_node(_parameter_schema(parameter.parameter))) * ", content = " * content * @@ -2872,30 +2878,45 @@ function _media_descriptor(media, normalized) end encoded = "(" * join(encodings, ',') * (length(encodings) == 1 ? "," : "") * ")" + # Form and multipart request decoding needs each top-level property's + # shape so single-valued exploded arrays still decode as arrays. + fields = "()" + if base_media_type == "application/x-www-form-urlencoded" || + startswith(base_media_type, "multipart/") + field_items = String[ + "(name = " * repr(field_name) * + ", shape = " * repr(_schema_shape(field_view)) * ")" for + (field_name, field_view) in sort(collect(properties); by = first) + ] + fields = "(" * join(field_items, ',') * + (length(field_items) == 1 ? "," : "") * ")" + end push!( items, "(" * repr(entry.first) * ", " * entry.second * ", " * _schema_descriptor(_schema_node(source.schema)) * ", " * encoded * - ")", + ", " * fields * ")", ) end return "(" * join(items, ',') * (length(items) == 1 ? "," : "") * ")" end -function _schema_shape(handle::Union{Nothing,SchemaHandle}) - handle === nothing && return :scalar - view = _resolved_view(SchemaView(handle)) - _is_object_schema(view) && return :object - types = _schema_types(view.value) +function _schema_shape(view::SchemaView) + resolved = _resolved_view(view) + _is_object_schema(resolved) && return :object + types = _schema_types(resolved.value) ( "array" in types || - view.value isa AbstractDict && - (haskey(view.value, "items") || haskey(view.value, "prefixItems")) + resolved.value isa AbstractDict && + (haskey(resolved.value, "items") || haskey(resolved.value, "prefixItems")) ) && return :array return :scalar end +_schema_shape(handle::Union{Nothing,SchemaHandle}) = + handle === nothing ? :scalar : _schema_shape(SchemaView(handle)) + function _header_descriptor(header::NormalizedHeader, type::String) schema = header.schema !== nothing ? header.schema : isempty(header.content) ? nothing : first(header.content).schema @@ -3106,6 +3127,7 @@ function _generate(plan::ClientPlan) _emit_security(io, plan) _emit_schema_data(io, plan) default_server = _default_server(plan.api) + print(io, GENERATED_RUNTIME_COMMON, '\n') print(io, GENERATED_RUNTIME, '\n') println(io, "const _DEFAULT_SERVER = ", repr(default_server)) println(io, "const SERVER = Ref{String}(_DEFAULT_SERVER)") diff --git a/src/planning.jl b/src/planning.jl index 95ded2a..624b39e 100644 --- a/src/planning.jl +++ b/src/planning.jl @@ -71,6 +71,19 @@ struct ClientPlan uses_base64::Bool end +struct ServerPlan + api::NormalizedAPI + module_name::String + models::Tuple{Vararg{ModelPlan}} + operations::Tuple{Vararg{OperationPlan}} + diagnostics::Tuple{Vararg{Diagnostic}} + uses_dates::Bool + uses_uuids::Bool + uses_base64::Bool +end + +const GenerationPlan = Union{ClientPlan,ServerPlan} + struct SchemaView value::Any node::Resources.NodeId @@ -1503,19 +1516,8 @@ function _check_generation_support!(context::PlanningContext, strict::Bool) return end -"""Build the deterministic Julia model and operation plan used by code generation.""" -function plan( - source; - name::AbstractString = "ApiClient", - strict::Bool = true, - max_diagnostics::Integer = 1_000, - kwargs..., -) - api = source isa NormalizedAPI ? source : - normalize(source; strict, max_diagnostics, kwargs...) - bag = DiagnosticBag(max_diagnostics) - context = PlanningContext(api, bag) - _check_generation_support!(context, strict) +function _plan_components!(context::PlanningContext) + api = context.api # Reserve stable component names before recursive planning starts. for (component_name, handle) in sort(collect(api.schemas); by = first) view = SchemaView(handle) @@ -1536,9 +1538,31 @@ function plan( for operation in ordered push!(operations, _plan_operation!(context, operation, used_functions)) end - diagnostics = Diagnostic[api.diagnostics...] - append!(diagnostics, bag.diagnostics) - _throw_on_errors("Cannot plan Julia client", diagnostics) + return operations +end + +function _finish_diagnostics(context::PlanningContext, summary::AbstractString) + diagnostics = Diagnostic[context.api.diagnostics...] + append!(diagnostics, context.bag.diagnostics) + _throw_on_errors(summary, diagnostics) + return Tuple(diagnostics) +end + +"""Build the deterministic Julia model and operation plan used by code generation.""" +function plan( + source; + name::AbstractString = "ApiClient", + strict::Bool = true, + max_diagnostics::Integer = 1_000, + kwargs..., +) + api = source isa NormalizedAPI ? source : + normalize(source; strict, max_diagnostics, kwargs...) + bag = DiagnosticBag(max_diagnostics) + context = PlanningContext(api, bag) + _check_generation_support!(context, strict) + operations = _plan_components!(context) + diagnostics = _finish_diagnostics(context, "Cannot plan Julia client") # Recursive planning emits dependencies before parents. Sort only aliases and # enums that do not depend on declaration order; object order stays topological. return ClientPlan( @@ -1546,7 +1570,85 @@ function plan( _type_identifier(name), Tuple(context.models), Tuple(operations), - Tuple(diagnostics), + diagnostics, + context.uses_dates, + context.uses_uuids, + context.uses_base64, + ) +end + +function _check_server_generation_support!(context::PlanningContext, strict::Bool) + for operation in context.api.operations + operation.direction === :request || continue + # A form-style exploded object query or cookie parameter consumes + # arbitrary wire names. One such parameter per location decodes from the + # pairs no other declared parameter claimed; two or more are ambiguous. + for location in (:query, :cookie) + exploded = NormalizedParameter[ + parameter for parameter in operation.parameters + if parameter.location === location && + parameter.style === :form && + parameter.explode === true && + _schema_shape(_parameter_schema(parameter)) === :object + ] + length(exploded) > 1 && _error!( + context.bag, + :ambiguous_exploded_object_parameters, + "multiple form-style exploded object $location parameters cannot be decoded unambiguously", + SourceLocation( + exploded[2].provenance.node.resource, + exploded[2].provenance.node.pointer, + ), + ) + end + operation.request_body === nothing && continue + for media in operation.request_body.content + base = _planning_content_base(media.content_type) + if startswith(base, "multipart/") && base != "multipart/form-data" + _error!( + context.bag, + :unsupported_multipart_server_generation, + "server generation only decodes multipart/form-data request bodies", + SourceLocation( + media.provenance.node.resource, + media.provenance.node.pointer, + ), + ) + end + end + end + return +end + +""" + OpenAPI.serverplan(source; name="ApiServer", strict=true, options...) -> ServerPlan + +Build the deterministic Julia model and operation plan used by server stub +generation. Accepts the same sources and options as [`OpenAPI.plan`](@ref) and +additionally rejects documents whose requests cannot be decoded faithfully on +the server side. +""" +function serverplan( + source; + name::AbstractString = "ApiServer", + strict::Bool = true, + max_diagnostics::Integer = 1_000, + kwargs..., +) + api = source isa NormalizedAPI ? source : + normalize(source; strict, max_diagnostics, kwargs...) + bag = DiagnosticBag(max_diagnostics) + context = PlanningContext(api, bag) + _check_generation_support!(context, strict) + _check_server_generation_support!(context, strict) + operations = _plan_components!(context) + diagnostics = _finish_diagnostics(context, "Cannot plan Julia server") + return ServerPlan( + api, + _type_identifier(name), + Tuple(context.models), + Tuple(operations), + diagnostics, context.uses_dates, context.uses_uuids, context.uses_base64, diff --git a/src/servergen.jl b/src/servergen.jl new file mode 100644 index 0000000..f1b850a --- /dev/null +++ b/src/servergen.jl @@ -0,0 +1,867 @@ +# Server-stub generation. The framework-neutral request engine below is pasted +# into generated server modules after GENERATED_RUNTIME_COMMON; framework +# packages (HTTP.jl through OpenAPIHTTPExt, Servo.jl through ServoOpenAPIExt) +# add `OpenAPI.server_source` methods that wrap it with router glue. + +const GENERATED_RUNTIME_SERVER = raw""" +struct _RequestFailure <: Exception + status::Int + message::String +end +Base.showerror(io::IO, error::_RequestFailure) = print(io, error.message) + +_is_hex_byte(byte::UInt8) = + UInt8('0') <= byte <= UInt8('9') || + UInt8('a') <= byte <= UInt8('f') || + UInt8('A') <= byte <= UInt8('F') + +function _percent_decode(text::AbstractString; plus_space::Bool = false) + bytes = codeunits(String(text)) + occursin('%', text) || (plus_space && occursin('+', text)) || + return String(text) + io = IOBuffer() + index = 1 + while index <= length(bytes) + byte = bytes[index] + if byte == UInt8('%') && index + 2 <= length(bytes) && + _is_hex_byte(bytes[index + 1]) && _is_hex_byte(bytes[index + 2]) + write(io, parse(UInt8, String(Char.(bytes[index + 1:index + 2])); base = 16)) + index += 3 + elseif plus_space && byte == UInt8('+') + write(io, ' ') + index += 1 + else + write(io, byte) + index += 1 + end + end + output = String(take!(io)) + isvalid(output) || + throw(_RequestFailure(400, "invalid percent-encoding in request")) + return output +end + +# Pair keys are percent-decoded; values stay raw so style delimiters emitted by +# clients (which escape delimiter characters inside items) survive splitting. +function _wire_pairs(text::AbstractString, pair_delimiters; plus_space::Bool = false) + output = Pair{String,String}[] + for token in split(text, char -> char in pair_delimiters) + stripped = strip(token) + isempty(stripped) && continue + raw = split(stripped, '='; limit = 2) + key = _percent_decode(raw[1]; plus_space) + push!(output, key => (length(raw) == 2 ? String(raw[2]) : "")) + end + return output +end + +_query_pairs(query::AbstractString) = _wire_pairs(query, ('&',); plus_space = true) + +function _cookie_pairs(headers) + output = Pair{String,String}[] + for value in _header_values(headers, "Cookie") + append!(output, _wire_pairs(value, (';', '&'))) + end + return output +end + +_typed_scalar(type, text) = _header_scalar(_header_type_variant(type, :scalar), text) + +function _typed_array(type, items) + selected = _header_type_variant(type, :array) + element = selected <: AbstractVector ? eltype(selected) : String + return Any[_header_scalar(element, item) for item in items] +end + +function _object_from_kv_tokens(tokens, context; decode::Bool = false) + object = JSON.Object{String,Any}() + for token in tokens + pair = split(token, '='; limit = 2) + length(pair) == 2 || + throw(_RequestFailure(400, "invalid object value while " * context)) + key = decode ? _percent_decode(String(pair[1])) : String(pair[1]) + item = decode ? _percent_decode(String(pair[2])) : String(pair[2]) + object[key] = _header_atom(item) + end + return object +end + +function _object_from_alternating(tokens, context; decode::Bool = false) + iseven(length(tokens)) || + throw(_RequestFailure(400, "invalid object value while " * context)) + object = JSON.Object{String,Any}() + for index in 1:2:length(tokens) + key = decode ? _percent_decode(String(tokens[index])) : String(tokens[index]) + item = decode ? _percent_decode(String(tokens[index + 1])) : String(tokens[index + 1]) + object[key] = _header_atom(item) + end + return object +end + +function _finish_parameter(descriptor, value, context) + _validate_schema(descriptor.schema, _encode(value), context; direction = :input) + return _decode(descriptor.type, value) +end + +function _decode_parameter_content(descriptor, text, context) + entry = first(descriptor.content) + media = _base_media_type(entry[1]) + if _is_json_media(media) + value = _parse_json(text, context) + _validate_schema(entry[3], value, context; direction = :input) + return _decode(descriptor.type, value) + end + _validate_schema(entry[3], text, context; direction = :input) + return _decode(descriptor.type, text) +end + +function _decode_path_parameter(descriptor, raw) + text = String(raw) + context = string("decoding path parameter ", descriptor.name) + isempty(descriptor.content) || + return _decode_parameter_content(descriptor, _percent_decode(text), context) + style = descriptor.style + shape = descriptor.shape + explode = descriptor.explode + value = if style === :label + startswith(text, '.') || + throw(_RequestFailure(400, "invalid label value while " * context)) + body = SubString(text, 2) + if shape === :array + _typed_array( + descriptor.type, + [_percent_decode(String(item)) for item in split(body, explode ? '.' : ',')], + ) + elseif shape === :object + explode ? _object_from_kv_tokens(split(body, '.'), context; decode = true) : + _object_from_alternating(split(body, ','), context; decode = true) + else + _typed_scalar(descriptor.type, _percent_decode(String(body))) + end + elseif style === :matrix + startswith(text, ';') || + throw(_RequestFailure(400, "invalid matrix value while " * context)) + tokens = split(SubString(text, 2), ';') + if shape === :array && explode + items = String[] + for token in tokens + pair = split(token, '='; limit = 2) + _percent_decode(String(pair[1])) == descriptor.name || continue + push!(items, length(pair) == 2 ? _percent_decode(String(pair[2])) : "") + end + _typed_array(descriptor.type, items) + elseif shape === :object && explode + _object_from_kv_tokens(tokens, context; decode = true) + else + payload = nothing + for token in tokens + pair = split(token, '='; limit = 2) + _percent_decode(String(pair[1])) == descriptor.name || continue + payload = length(pair) == 2 ? String(pair[2]) : "" + break + end + payload === nothing && + throw(_RequestFailure(400, "missing matrix value while " * context)) + if shape === :array + _typed_array( + descriptor.type, + [_percent_decode(String(item)) for item in split(payload, ',')], + ) + elseif shape === :object + _object_from_alternating(split(payload, ','), context; decode = true) + else + _typed_scalar(descriptor.type, _percent_decode(payload)) + end + end + else # simple + if shape === :array + _typed_array( + descriptor.type, + [_percent_decode(String(item)) for item in split(text, ',')], + ) + elseif shape === :object + explode ? _object_from_kv_tokens(split(text, ','), context; decode = true) : + _object_from_alternating(split(text, ','), context; decode = true) + else + _typed_scalar(descriptor.type, _percent_decode(text)) + end + end + return _finish_parameter(descriptor, value, context) +end + +function _first_pair_index(pairs, name) + for (index, pair) in enumerate(pairs) + pair.first == name && return index + end + return nothing +end + +function _decode_form_parameter(descriptor, pairs, consumed, context; plus_space::Bool) + name = descriptor.name + decoded(raw) = _percent_decode(String(raw); plus_space) + if descriptor.shape === :array + if descriptor.explode + items = String[] + for (index, pair) in enumerate(pairs) + pair.first == name || continue + consumed[index] = true + push!(items, decoded(pair.second)) + end + isempty(items) && return ABSENT + return _finish_parameter(descriptor, _typed_array(descriptor.type, items), context) + end + index = _first_pair_index(pairs, name) + index === nothing && return ABSENT + consumed[index] = true + items = [decoded(item) for item in split(pairs[index].second, ',')] + return _finish_parameter(descriptor, _typed_array(descriptor.type, items), context) + elseif descriptor.shape === :object + # Exploded objects consume the wire names no other parameter claimed; + # the caller collects them after every named parameter is decoded. + descriptor.explode && return ABSENT + index = _first_pair_index(pairs, name) + index === nothing && return ABSENT + consumed[index] = true + tokens = [decoded(item) for item in split(pairs[index].second, ',')] + return _finish_parameter( + descriptor, + _object_from_alternating(tokens, context), + context, + ) + end + index = _first_pair_index(pairs, name) + index === nothing && return ABSENT + consumed[index] = true + return _finish_parameter( + descriptor, + _typed_scalar(descriptor.type, decoded(pairs[index].second)), + context, + ) +end + +function _decode_query_parameter(descriptor, pairs, consumed) + context = string("decoding query parameter ", descriptor.name) + name = descriptor.name + if !isempty(descriptor.content) + index = _first_pair_index(pairs, name) + index === nothing && return ABSENT + consumed[index] = true + return _decode_parameter_content( + descriptor, + _percent_decode(String(pairs[index].second); plus_space = true), + context, + ) + end + style = descriptor.style + if style === :deepObject + object = JSON.Object{String,Any}() + items = Any[] + scalar = nothing + found = false + prefix = name * "[" + list_key = name * "[]" + for (index, pair) in enumerate(pairs) + key = pair.first + value() = _percent_decode(String(pair.second); plus_space = true) + if key == list_key + push!(items, _header_atom(value())) + elseif startswith(key, prefix) && endswith(key, ']') && + ncodeunits(key) > ncodeunits(prefix) + 1 + inner = String(SubString( + key, + ncodeunits(prefix) + 1, + ncodeunits(key) - 1, + )) + object[inner] = _header_atom(value()) + elseif key == name + scalar = _header_atom(value()) + else + continue + end + consumed[index] = true + found = true + end + found || return ABSENT + value = !isempty(object) ? object : (!isempty(items) ? items : scalar) + return _finish_parameter(descriptor, value, context) + elseif style === :spaceDelimited || style === :pipeDelimited + index = _first_pair_index(pairs, name) + index === nothing && return ABSENT + consumed[index] = true + delimiter = style === :spaceDelimited ? ' ' : '|' + tokens = split( + _percent_decode(String(pairs[index].second); plus_space = true), + delimiter, + ) + if descriptor.shape === :object + return _finish_parameter( + descriptor, + _object_from_alternating(String.(tokens), context), + context, + ) + end + return _finish_parameter( + descriptor, + _typed_array(descriptor.type, String.(tokens)), + context, + ) + end + return _decode_form_parameter(descriptor, pairs, consumed, context; plus_space = true) +end + +function _decode_cookie_parameter(descriptor, pairs, consumed) + context = string("decoding cookie parameter ", descriptor.name) + if !isempty(descriptor.content) + index = _first_pair_index(pairs, descriptor.name) + index === nothing && return ABSENT + consumed[index] = true + return _decode_parameter_content( + descriptor, + _percent_decode(String(pairs[index].second)), + context, + ) + end + return _decode_form_parameter(descriptor, pairs, consumed, context; plus_space = false) +end + +function _decode_header_parameter(descriptor, headers) + values = _header_values(headers, descriptor.name) + isempty(values) && return ABSENT + context = string("decoding header parameter ", descriptor.name) + isempty(descriptor.content) || + return _decode_parameter_content(descriptor, join(values, ','), context) + return _decode_schema_header( + descriptor.type, + values, + descriptor.shape, + descriptor.explode === true, + descriptor.schema; + direction = :input, + context, + ) +end + +function _collect_exploded_object(descriptor, pairs, consumed, context; plus_space::Bool) + object = JSON.Object{String,Any}() + for (index, pair) in enumerate(pairs) + consumed[index] && continue + object[pair.first] = + _header_atom(_percent_decode(String(pair.second); plus_space)) + consumed[index] = true + end + isempty(object) && !descriptor.required && return ABSENT + return _finish_parameter(descriptor, object, context) +end + +function _encoding_for(encodings, name) + for encoding in encodings + encoding.name == name && return encoding + end + return nothing +end + +function _field_shape(fields, name) + for field in fields + field.name == name && return field.shape + end + return :scalar +end + +function _push_form_value!(object, fields, name, value) + if haskey(object, name) + existing = object[name] + object[name] = existing isa Vector{Any} ? push!(existing, value) : + Any[existing, value] + elseif _field_shape(fields, name) === :array && !(value isa Vector{Any}) + object[name] = Any[value] + else + object[name] = value + end + return object +end + +function _form_body_object(body, encodings, fields) + object = JSON.Object{String,Any}() + text = String(copy(body)) + isvalid(text) || + throw(_RequestFailure(400, "form request body is not valid UTF-8")) + for pair in _wire_pairs(text, ('&',); plus_space = true) + name = pair.first + encoding = _encoding_for(encodings, name) + media = encoding === nothing ? "" : + _base_media_type(something(encoding.content_type, "")) + value = if !isempty(media) && _is_json_media(media) + _parse_json( + _percent_decode(String(pair.second); plus_space = true), + "decoding form field " * name, + ) + elseif encoding !== nothing && encoding.explode === false + delimiter = encoding.style === :spaceDelimited ? ' ' : + encoding.style === :pipeDelimited ? '|' : ',' + Any[ + _header_atom(_percent_decode(String(item); plus_space = true)) + for item in split(pair.second, delimiter) + ] + else + _header_atom(_percent_decode(String(pair.second); plus_space = true)) + end + _push_form_value!(object, fields, name, value) + end + return object +end + +function _multipart_body_object(parts, encodings, fields) + object = JSON.Object{String,Any}() + for part in parts + name = String(part.name) + encoding = _encoding_for(encodings, name) + declared = something(part.content_type, "") + media = _base_media_type( + !isempty(declared) ? declared : + encoding === nothing ? "" : something(encoding.content_type, ""), + ) + # Text parts stay strings; binary parts become base64 so schema + # validation sees JSON-like values and base64-typed fields round trip. + value = if _is_json_media(media) + _parse_json(part.data, "decoding multipart field " * name) + elseif isvalid(String, part.data) + String(copy(part.data)) + else + Base64.base64encode(part.data) + end + _push_form_value!(object, fields, name, value) + end + return object +end + +function _decode_request_body(operation, headers, body, parts) + request = operation.request + request === nothing && return (false, nothing) + content_values = _header_values(headers, "Content-Type") + content_type = isempty(content_values) ? "" : first(content_values) + if isempty(body) && parts === nothing && isempty(content_type) + request.required && + throw(_RequestFailure(400, "missing required request body")) + return (false, nothing) + end + entry = isempty(content_type) ? first(request.media) : + _select_media(request.media, content_type) + entry === nothing && + throw(UnsupportedMediaType(String(content_type), :request)) + media = _base_media_type(isempty(content_type) ? entry[1] : content_type) + type = entry[2] + schema = entry[3] + encodings = entry[4] + fields = entry[5] + context = "decoding the request body" + if media == "multipart/form-data" + parts === nothing && + throw(_RequestFailure(400, "invalid multipart request body")) + value = _multipart_body_object(parts, encodings, fields) + _validate_schema(schema, _encode(value), context; direction = :input) + return (true, _decode(type, value)) + elseif media == "application/x-www-form-urlencoded" + value = _form_body_object(body, encodings, fields) + _validate_schema(schema, value, context; direction = :input) + return (true, _decode(type, value)) + elseif _is_json_media(media) + if isempty(body) + request.required && + throw(_RequestFailure(400, "missing required request body")) + return (false, nothing) + end + value = _parse_json(body, context) + _validate_schema(schema, value, context; direction = :input) + return (true, _decode(type, value)) + elseif _is_sequential_json_media(media) + value = _decode_sequential_json(body, media) + _validate_schema(schema, value, context; direction = :input) + return (true, _decode(type, value)) + elseif startswith(media, "text/") || type === String + isvalid(String, body) || + throw(_RequestFailure(400, "text request body is not valid UTF-8")) + value = String(copy(body)) + _validate_schema(schema, value, context; direction = :input) + return (true, _decode(type, value)) + end + _validate_schema(schema, Base64.base64encode(body), context; direction = :input) + return (true, type === Any ? copy(body) : _decode(type, copy(body))) +end + +function _operation_arguments(entry, path_params, query_string, headers, body, parts) + operation = entry.operation + query = _query_pairs(query_string) + query_consumed = falses(length(query)) + cookies = nothing + cookie_consumed = nothing + values = Dict{Symbol,Any}() + exploded_query = nothing + exploded_cookie = nothing + for descriptor in operation.parameters + decoded = try + if descriptor.location === :path + raw = get(path_params, descriptor.name, nothing) + raw === nothing ? + throw(_RequestFailure(400, string("missing path parameter ", descriptor.name))) : + _decode_path_parameter(descriptor, raw) + elseif descriptor.location === :query + if isempty(descriptor.content) && descriptor.style === :form && + descriptor.explode === true && descriptor.shape === :object + exploded_query = descriptor + continue + end + _decode_query_parameter(descriptor, query, query_consumed) + elseif descriptor.location === :header + _decode_header_parameter(descriptor, headers) + elseif descriptor.location === :cookie + if cookies === nothing + cookies = _cookie_pairs(headers) + cookie_consumed = falses(length(cookies)) + end + if isempty(descriptor.content) && descriptor.style === :form && + descriptor.explode === true && descriptor.shape === :object + exploded_cookie = descriptor + continue + end + _decode_cookie_parameter(descriptor, cookies, cookie_consumed) + else + throw(_RequestFailure( + 400, + string("unsupported parameter location ", descriptor.location), + )) + end + catch error + error isa DecodeError ? throw(DecodeError(string( + "invalid ", + descriptor.location, + " parameter ", + descriptor.name, + ": ", + error.message, + ))) : rethrow() + end + if decoded isa Absent + descriptor.required && throw(_RequestFailure( + 400, + string("missing required ", descriptor.location, " parameter ", descriptor.name), + )) + continue + end + values[descriptor.arg] = decoded + end + if exploded_query !== nothing + decoded = _collect_exploded_object( + exploded_query, + query, + query_consumed, + string("decoding query parameter ", exploded_query.name); + plus_space = true, + ) + decoded isa Absent || (values[exploded_query.arg] = decoded) + end + if exploded_cookie !== nothing + decoded = _collect_exploded_object( + exploded_cookie, + cookies, + cookie_consumed, + string("decoding cookie parameter ", exploded_cookie.name); + plus_space = false, + ) + decoded isa Absent || (values[exploded_cookie.arg] = decoded) + end + has_body, decoded_body = _decode_request_body(operation, headers, body, parts) + args = Any[values[arg] for arg in entry.path_args] + entry.required_body && begin + has_body || throw(_RequestFailure(400, "missing required request body")) + push!(args, decoded_body) + end + kwargs = Pair{Symbol,Any}[] + for descriptor in operation.parameters + descriptor.location === :path && continue + haskey(values, descriptor.arg) || continue + push!(kwargs, descriptor.arg => values[descriptor.arg]) + end + !entry.required_body && has_body && push!(kwargs, :body => decoded_body) + return args, kwargs +end + +function _selector_status(selector) + normalized = uppercase(String(selector)) + all(isdigit, normalized) && return parse(Int, normalized) + return 200 +end + +function _success_response(responses) + range = nothing + fallback = nothing + for response in responses + selector = uppercase(response.selector) + if !isempty(selector) && all(isdigit, selector) && startswith(selector, '2') + return response + elseif selector == "2XX" + range === nothing && (range = response) + elseif selector == "DEFAULT" + fallback === nothing && (fallback = response) + end + end + return something(range, fallback, Some(nothing)) +end + +function _server_response(operation, result) + result === nothing && return (204, Pair{String,String}[], UInt8[]) + descriptor = _success_response(operation.responses) + if descriptor === nothing || isempty(descriptor.media) + throw(ArgumentError(string( + "operation ", + operation.id, + " documents no success response content; return `nothing` or a framework response", + ))) + end + status = _selector_status(descriptor.selector) + index = something( + findfirst(entry -> _is_json_media(_base_media_type(entry[1])), descriptor.media), + 1, + ) + entry = descriptor.media[index] + media = _base_media_type(entry[1]) + context = string("encoding the ", operation.id, " response body") + if _is_json_media(media) + lowered = _encode(result) + _validate_schema(entry[3], lowered, context; direction = :output) + payload = Vector{UInt8}(codeunits(JSON.json(lowered))) + elseif _is_sequential_json_media(media) + lowered = _encode(result) + _validate_schema(entry[3], lowered, context; direction = :output) + payload = Vector{UInt8}(codeunits(_encode_sequential_json(result, media))) + elseif startswith(media, "text/") + result isa AbstractString || throw(ArgumentError(string( + "operation ", + operation.id, + " documents a text response; return an AbstractString or a framework response", + ))) + _validate_schema(entry[3], String(result), context; direction = :output) + payload = Vector{UInt8}(codeunits(String(result))) + else + bytes = result isa AbstractVector{UInt8} ? Vector{UInt8}(result) : + result isa AbstractString ? Vector{UInt8}(codeunits(String(result))) : + throw(ArgumentError(string( + "operation ", + operation.id, + " documents a binary response; return bytes, a string, or a framework response", + ))) + _validate_schema(entry[3], Base64.base64encode(bytes), context; direction = :output) + payload = bytes + end + headers = Pair{String,String}[_safe_header("Content-Type", entry[1])] + return (status, headers, payload) +end + +function _error_payload(status, message) + payload = JSON.json((; error = (; message = String(message)))) + return ( + status, + Pair{String,String}["Content-Type" => "application/json"], + Vector{UInt8}(codeunits(payload)), + ) +end + +# Returns a (status, headers, body) triple for request decoding failures the +# operation contract anticipates, and rethrows everything else so the hosting +# framework reports a genuine server error. +function _request_error_response(error) + error isa _RequestFailure && return _error_payload(error.status, error.message) + error isa DecodeError && return _error_payload(400, error.message) + error isa SchemaValidationError && + return _error_payload(400, sprint(showerror, error)) + error isa UnsupportedMediaType && + return _error_payload(415, sprint(showerror, error)) + throw(error) +end + +_response_error_payload(error) = _error_payload(500, sprint(showerror, error)) +""" + +function _server_stub_signature(operation::OperationPlan) + positional = String["request"] + path_parameters = _ordered_path_parameters(operation) + path_names = Set{String}(parameter.name for parameter in path_parameters) + for parameter in path_parameters + push!(positional, string(parameter.name, "::", parameter.type)) + end + if operation.request_body !== nothing && operation.request_body.required + push!(positional, "body::" * operation.request_body.type) + end + keywords = String[] + for parameter in operation.parameters + parameter.name in path_names && continue + parameter.required && push!(keywords, parameter.name * "::" * parameter.type) + end + for parameter in operation.parameters + parameter.name in path_names && continue + parameter.required || + push!(keywords, parameter.name * "::" * parameter.type * " = ABSENT") + end + if operation.request_body !== nothing && !operation.request_body.required + push!(keywords, "body::" * operation.request_body.type * " = ABSENT") + end + text = operation.name * "(" * join(positional, ", ") + isempty(keywords) || (text *= "; " * join(keywords, ", ")) + return text * ") -> " * operation.return_type +end + +function _emit_server_operations(io::IO, plan::ServerPlan) + constants = String[] + for operation in plan.operations + push!(constants, _emit_operation_descriptor(io, operation, plan.api)) + end + println(io, "const _SERVER_OPS = (") + for (operation, const_name) in zip(plan.operations, constants) + path_parameters = _ordered_path_parameters(operation) + path_args = join( + (repr(Symbol(parameter.name)) for parameter in path_parameters), + ", ", + ) + required_body = operation.request_body !== nothing && + operation.request_body.required + println( + io, + " (operation = ", + const_name, + ", invoke = ", + repr(Symbol(operation.name)), + ", method = ", + repr(String(operation.operation.method)), + ", path = ", + repr(operation.operation.path), + ", path_args = (", + path_args, + length(path_parameters) == 1 ? "," : "", + "), required_body = ", + required_body ? "true" : "false", + ", signature = ", + repr(_server_stub_signature(operation)), + "),", + ) + end + println(io, ")\n") +end + +""" + OpenAPI.server_module_source(plan; imports, glue) -> String + +Assemble a generated server module for a framework extension: the shared +generated runtime, models, operation descriptors, and the `_SERVER_OPS` route +table, wrapped between the extension's `imports` line and its router `glue` +source. Framework extensions call this from their `OpenAPI.server_source` +methods; it is not intended for direct use. +""" +function server_module_source( + plan::ServerPlan; + imports::AbstractString, + glue::AbstractString, +) + io = IOBuffer() + println( + io, + "# Generated by OpenAPI.jl from ", + repr(plan.api.title), + " version ", + plan.api.api_version, + ". Do not edit.", + ) + println(io, "# Implement these handler functions in a module (or any value") + println(io, "# supporting `getfield`) and mount them with `register!(router, impl)`:") + for operation in plan.operations + println(io, "# ", _server_stub_signature(operation)) + end + println(io, "module ", plan.module_name, "\n") + println(io, imports) + println(io, "const SchemaEngine = OpenAPI.SchemaEngine\n") + _emit_security(io, plan) + _emit_schema_data(io, plan) + print(io, GENERATED_RUNTIME_COMMON, '\n') + print(io, GENERATED_RUNTIME_SERVER, '\n') + indices = _model_indices(plan) + wrapped_aliases = _cyclic_aliases(plan) + abstract_targets = _forward_abstracts(plan, wrapped_aliases) + for target in sort(collect(abstract_targets)) + println(io, "abstract type Abstract", target, " end") + end + isempty(abstract_targets) || println(io) + for (index, model) in enumerate(plan.models) + _emit_model(io, model, index, indices, abstract_targets, wrapped_aliases) + end + _emit_server_operations(io, plan) + print(io, glue, '\n') + println(io, "end # module ", plan.module_name) + return String(take!(io)) +end + +""" + OpenAPI.server_source(::Val{framework}, plan::ServerPlan) -> String + +Extension seam for framework-specific server-stub emission. Loading HTTP.jl +adds the `Val{:HTTP}` method; server framework packages such as Servo.jl add +their own. [`OpenAPI.server`](@ref) dispatches here. +""" +function server_source end + +function _loaded_server_frameworks() + frameworks = String[] + for method in methods(server_source) + signature = method.sig + signature isa DataType || continue + length(signature.parameters) >= 2 || continue + valtype = signature.parameters[2] + valtype isa DataType && valtype <: Val && isconcretetype(valtype) || continue + push!(frameworks, string(only(valtype.parameters))) + end + return sort(frameworks) +end + +""" + OpenAPI.server(source; framework=:HTTP, name="ApiServer", path=nothing, strict=true, options...) -> String + +Generate a deterministic Julia server-stub module after full OpenAPI loading, +reference binding, semantic normalization, and type planning. The generated +module decodes typed request parameters and bodies, dispatches to handler +functions you implement (one per operation, listed in the generated header), +validates and encodes responses, and mounts on the chosen framework's router +through its `register!(router, impl)` function. + +`framework` selects the emitter: `:HTTP` (available when HTTP.jl is loaded) +targets `HTTP.Router`; server framework packages can add their own through the +[`OpenAPI.server_source`](@ref) extension seam. Accepts the same `source` +values and keyword options as [`OpenAPI.client`](@ref). +""" +function server( + source; + framework::Union{Symbol,AbstractString} = :HTTP, + name::AbstractString = "ApiServer", + path::Union{Nothing,AbstractString} = nothing, + strict::Bool = true, + kwargs..., +) + server_plan = source isa ServerPlan ? source : + serverplan(source; name, strict, kwargs...) + key = Symbol(framework) + if !hasmethod(server_source, Tuple{Val{key},ServerPlan}) + loaded = _loaded_server_frameworks() + hint = isempty(loaded) ? + "load a framework package first (`using HTTP` enables framework = :HTTP)" : + "loaded frameworks: " * join(loaded, ", ") + throw(ArgumentError(string( + "no server generator is loaded for framework ", + repr(key), + "; ", + hint, + ))) + end + output = server_source(Val(key), server_plan) + if path !== nothing + open(path, "w") do io + write(io, output) + end + end + return output +end diff --git a/test/runtests.jl b/test/runtests.jl index 7746c52..f1dfd81 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -254,6 +254,7 @@ include("references.jl") include("semantics.jl") include("runtime.jl") include("runtime_integration.jl") +include("servergen.jl") include("trim_compile_tests.jl") if haskey(ENV, "OPENAPI_CORPUS_TESTS") include("corpus.jl") diff --git a/test/servergen.jl b/test/servergen.jl new file mode 100644 index 0000000..39e79e9 --- /dev/null +++ b/test/servergen.jl @@ -0,0 +1,614 @@ +function server_parameter(name, location, schema; kwargs...) + entries = Any["name" => name, "in" => location, "schema" => schema] + for (key, value) in kwargs + push!(entries, replace(String(key), "_" => "") => value) + end + return OpenAPI.obj(entries...) +end + +const SERVER_ROUNDTRIP_DOCUMENT = OpenAPI.obj( + "openapi" => "3.1.0", + "info" => OpenAPI.obj("title" => "Server Round Trip", "version" => "1.0.0"), + "paths" => OpenAPI.obj( + "/styles/{simple}/{label}/{matrix}/{mexp}" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "styles", + "parameters" => Any[ + server_parameter( + "simple", + "path", + OpenAPI.obj("type" => "array", "items" => OpenAPI.obj("type" => "string")); + required = true, + ), + server_parameter( + "label", + "path", + OpenAPI.obj("type" => "string"); + required = true, + style = "label", + ), + server_parameter( + "matrix", + "path", + OpenAPI.obj("type" => "array", "items" => OpenAPI.obj("type" => "integer")); + required = true, + style = "matrix", + ), + server_parameter( + "mexp", + "path", + OpenAPI.obj("type" => "array", "items" => OpenAPI.obj("type" => "integer")); + required = true, + style = "matrix", + explode = true, + ), + server_parameter( + "qform", + "query", + OpenAPI.obj("type" => "array", "items" => OpenAPI.obj("type" => "string")); + required = true, + explode = false, + ), + server_parameter( + "qexp", + "query", + OpenAPI.obj("type" => "array", "items" => OpenAPI.obj("type" => "string")), + ), + server_parameter( + "spaces", + "query", + OpenAPI.obj("type" => "array", "items" => OpenAPI.obj("type" => "string")); + style = "spaceDelimited", + explode = false, + ), + server_parameter( + "pipes", + "query", + OpenAPI.obj("type" => "array", "items" => OpenAPI.obj("type" => "string")); + style = "pipeDelimited", + explode = false, + ), + server_parameter( + "deep", + "query", + OpenAPI.obj( + "type" => "object", + "properties" => OpenAPI.obj( + "a" => OpenAPI.obj("type" => "string"), + "b" => OpenAPI.obj("type" => "integer"), + ), + ); + style = "deepObject", + explode = true, + ), + server_parameter( + "obj", + "query", + OpenAPI.obj( + "type" => "object", + "properties" => OpenAPI.obj( + "x" => OpenAPI.obj("type" => "string"), + "y" => OpenAPI.obj("type" => "integer"), + ), + ); + explode = true, + ), + server_parameter( + "X-Items", + "header", + OpenAPI.obj("type" => "array", "items" => OpenAPI.obj("type" => "integer")), + ), + server_parameter("sess", "cookie", OpenAPI.obj("type" => "string")), + server_parameter( + "csv", + "cookie", + OpenAPI.obj("type" => "array", "items" => OpenAPI.obj("type" => "string")); + explode = false, + ), + ], + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "echo", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj("\$ref" => "#/components/schemas/StylesEcho"), + ), + ), + ), + ), + ), + ), + "/form" => OpenAPI.obj( + "post" => OpenAPI.obj( + "operationId" => "submitForm", + "requestBody" => OpenAPI.obj( + "required" => true, + "content" => OpenAPI.obj( + "application/x-www-form-urlencoded" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "type" => "object", + "required" => Any["q"], + "properties" => OpenAPI.obj( + "q" => OpenAPI.obj("type" => "string"), + "limit" => OpenAPI.obj("type" => "integer"), + "tags" => OpenAPI.obj( + "type" => "array", + "items" => OpenAPI.obj("type" => "string"), + ), + ), + ), + ), + ), + ), + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "echo", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "type" => "object", + "properties" => OpenAPI.obj( + "q" => OpenAPI.obj("type" => "string"), + "limit" => OpenAPI.obj("type" => "integer"), + "tags" => OpenAPI.obj( + "type" => "array", + "items" => OpenAPI.obj("type" => "string"), + ), + ), + ), + ), + ), + ), + ), + ), + ), + "/upload" => OpenAPI.obj( + "post" => OpenAPI.obj( + "operationId" => "upload", + "requestBody" => OpenAPI.obj( + "required" => true, + "content" => OpenAPI.obj( + "multipart/form-data" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "type" => "object", + "required" => Any["note"], + "properties" => OpenAPI.obj( + "note" => OpenAPI.obj("type" => "string"), + "file" => OpenAPI.obj("type" => "string"), + ), + ), + ), + ), + ), + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "echo", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "type" => "object", + "properties" => OpenAPI.obj( + "note" => OpenAPI.obj("type" => "string"), + "bytes" => OpenAPI.obj("type" => "integer"), + ), + ), + ), + ), + ), + ), + ), + ), + "/text" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "textOut", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "text", + "content" => OpenAPI.obj( + "text/plain" => OpenAPI.obj( + "schema" => OpenAPI.obj("type" => "string"), + ), + ), + ), + ), + ), + ), + "/bin" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "binOut", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "bytes", + "content" => OpenAPI.obj( + "application/octet-stream" => OpenAPI.obj( + "schema" => OpenAPI.obj("type" => "string"), + ), + ), + ), + ), + ), + ), + "/custom" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "custom", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "text", + "content" => OpenAPI.obj( + "text/plain" => OpenAPI.obj( + "schema" => OpenAPI.obj("type" => "string"), + ), + ), + ), + ), + ), + ), + "/nothing" => OpenAPI.obj( + "put" => OpenAPI.obj( + "operationId" => "noContent", + "responses" => OpenAPI.obj( + "204" => OpenAPI.obj("description" => "empty"), + ), + ), + ), + ), + "components" => OpenAPI.obj( + "schemas" => OpenAPI.obj( + "StylesEcho" => OpenAPI.obj( + "type" => "object", + "properties" => OpenAPI.obj( + "simple" => OpenAPI.obj("type" => "array", "items" => OpenAPI.obj("type" => "string")), + "label" => OpenAPI.obj("type" => "string"), + "matrix" => OpenAPI.obj("type" => "array", "items" => OpenAPI.obj("type" => "integer")), + "mexp" => OpenAPI.obj("type" => "array", "items" => OpenAPI.obj("type" => "integer")), + "qform" => OpenAPI.obj("type" => "array", "items" => OpenAPI.obj("type" => "string")), + "qexp" => OpenAPI.obj("type" => "array", "items" => OpenAPI.obj("type" => "string")), + "spaces" => OpenAPI.obj("type" => "array", "items" => OpenAPI.obj("type" => "string")), + "pipes" => OpenAPI.obj("type" => "array", "items" => OpenAPI.obj("type" => "string")), + "deep" => OpenAPI.obj( + "type" => "object", + "properties" => OpenAPI.obj( + "a" => OpenAPI.obj("type" => "string"), + "b" => OpenAPI.obj("type" => "integer"), + ), + ), + "obj" => OpenAPI.obj( + "type" => "object", + "properties" => OpenAPI.obj( + "x" => OpenAPI.obj("type" => "string"), + "y" => OpenAPI.obj("type" => "integer"), + ), + ), + "items" => OpenAPI.obj("type" => "array", "items" => OpenAPI.obj("type" => "integer")), + "sess" => OpenAPI.obj("type" => "string"), + "csv" => OpenAPI.obj("type" => "array", "items" => OpenAPI.obj("type" => "string")), + ), + ), + ), + ), +) + +const SERVER_IMPL_SOURCE = """ +using HTTP + +function styles( + req, + simple, + label, + matrix, + mexp; + qform, + qexp = String[], + spaces = String[], + pipes = String[], + deep = nothing, + obj = nothing, + x_items = Int[], + sess = "", + csv = String[], +) + return (; + simple, + label, + matrix, + mexp, + qform, + qexp, + spaces, + pipes, + deep = something(deep, (;)), + obj = something(obj, (;)), + items = x_items, + sess, + csv, + ) +end + +submitform(req, body) = (; + q = body.q, + limit = body.limit isa Int ? body.limit : 0, + tags = body.tags isa Vector ? body.tags : String[], +) + +upload(req, body) = (; + note = body.note, + bytes = body.file isa AbstractString ? ncodeunits(body.file) : 0, +) + +textout(req) = "plain text" + +binout(req) = Vector{UInt8}(codeunits("raw-bytes")) + +custom(req) = HTTP.Response(418, ["Content-Type" => "text/plain"], "teapot") + +nocontent(req) = nothing +""" + +@testset "server generation" begin + server_source = OpenAPI.server(SERVER_ROUNDTRIP_DOCUMENT; name = "RoundTripServer") + @test server_source == OpenAPI.server(SERVER_ROUNDTRIP_DOCUMENT; name = "RoundTripServer") + @test startswith(server_source, "# Generated by OpenAPI.jl") + @test occursin("register!", server_source) + @test occursin( + "styles(request, simple::Vector{String}, label::String, matrix::Vector{Int64}, mexp::Vector{Int64};", + server_source, + ) + + server_host = Module(:ServerGenHost) + Base.include_string(server_host, server_source, "RoundTripServer.jl") + S = Base.invokelatest(getfield, server_host, :RoundTripServer) + sregister(args...; kwargs...) = + Base.invokelatest(getfield(S, :register!), args...; kwargs...) + + impl = Module(:ServerGenImpl) + Base.include_string(impl, SERVER_IMPL_SOURCE, "ServerGenImpl.jl") + + @testset "missing implementations are reported" begin + empty_impl = Module(:ServerGenEmptyImpl) + router = HTTP.Router() + error = try + sregister(router, empty_impl) + nothing + catch caught + caught + end + @test error isa ArgumentError + @test occursin("missing handler functions", error.msg) + @test occursin("styles(request", error.msg) + end + + @test Base.invokelatest(getfield, S, :register) === + Base.invokelatest(getfield, S, :register!) + + middleware_hits = Ref(0) + middleware = handler -> function (request) + middleware_hits[] += 1 + return handler(request) + end + router = HTTP.Router() + sregister(router, impl; middleware) + prefixed = HTTP.Router() + sregister(prefixed, impl; path_prefix = "/v3") + + server = HTTP.serve!(router, "127.0.0.1", 0; verbose = false) + prefixed_server = HTTP.serve!(prefixed, "127.0.0.1", 0; verbose = false) + try + port = HTTP.port(server) + base = "http://127.0.0.1:$port" + + client_source = OpenAPI.client(SERVER_ROUNDTRIP_DOCUMENT; name = "RoundTripClient") + client_host = Module(:ServerGenClientHost) + Base.include_string(client_host, client_source, "RoundTripClient.jl") + C = Base.invokelatest(getfield, client_host, :RoundTripClient) + call(name, args...; kwargs...) = + Base.invokelatest(getfield(C, name), args...; kwargs...) + call(:server!, base) + + @testset "parameter style round trip" begin + echo = call( + :styles, + ["a", "b/slash"], + "labelled", + [1, 2], + [3, 4]; + qform = ["q1", "q,2"], + qexp = ["e1", "e2"], + spaces = ["s1", "s2"], + pipes = ["p1", "p2"], + deep = call(:StylesDeep, "deep", 7, Dict{String,Any}()), + obj = call(:StylesObj, "ex", 9, Dict{String,Any}()), + x_items = [10, 11], + sess = "session-1", + csv = ["c1", "c2"], + ) + @test echo.simple == ["a", "b/slash"] + @test echo.label == "labelled" + @test echo.matrix == [1, 2] + @test echo.mexp == [3, 4] + @test echo.qform == ["q1", "q,2"] + @test echo.qexp == ["e1", "e2"] + @test echo.spaces == ["s1", "s2"] + @test echo.pipes == ["p1", "p2"] + @test echo.deep.a == "deep" + @test echo.deep.b == 7 + @test echo.obj.x == "ex" + @test echo.obj.y == 9 + @test echo.items == [10, 11] + @test echo.sess == "session-1" + @test echo.csv == ["c1", "c2"] + @test middleware_hits[] == 1 + end + + @testset "form and multipart bodies round trip" begin + form_body = call( + :SubmitformRequest, + "search term", + 5, + ["solo"], + Dict{String,Any}(), + ) + form_echo = call(:submitform, form_body) + @test form_echo.q == "search term" + @test form_echo.limit == 5 + @test form_echo.tags == ["solo"] + + upload_body = call( + :UploadModelRequest, + "note text", + "file contents here", + Dict{String,Any}(), + ) + upload_echo = call(:upload, upload_body) + @test upload_echo.note == "note text" + @test upload_echo.bytes == ncodeunits("file contents here") + end + + @testset "response encodings" begin + @test call(:textout) == "plain text" + @test call(:binout) == "raw-bytes" + @test call(:nocontent) === nothing + end + + @testset "framework response passthrough" begin + response = HTTP.get("$base/custom"; status_exception = false) + @test response.status == 418 + @test String(response.body) == "teapot" + end + + @testset "request error responses" begin + bad_path = HTTP.get( + "$base/styles/a/.l/;matrix=oops/;mexp=1?qform=x"; + status_exception = false, + ) + @test bad_path.status == 400 + @test occursin("matrix", String(bad_path.body)) + + missing_required = HTTP.get( + "$base/styles/a/.l/;matrix=1/;mexp=1"; + status_exception = false, + ) + @test missing_required.status == 400 + @test occursin("qform", String(missing_required.body)) + + invalid_body = HTTP.post( + "$base/form"; + headers = ["Content-Type" => "application/json"], + body = "{}", + status_exception = false, + ) + @test invalid_body.status == 415 + + unknown_route = HTTP.get("$base/absent"; status_exception = false) + @test unknown_route.status == 404 + end + + @testset "path prefix" begin + prefixed_port = HTTP.port(prefixed_server) + response = HTTP.get( + "http://127.0.0.1:$prefixed_port/v3/text"; + status_exception = false, + ) + @test response.status == 200 + @test String(response.body) == "plain text" + unprefixed = HTTP.get( + "http://127.0.0.1:$prefixed_port/text"; + status_exception = false, + ) + @test unprefixed.status == 404 + end + finally + close(server) + close(prefixed_server) + end +end + +@testset "server planning gates" begin + @testset "framework selection" begin + plan = OpenAPI.serverplan(SERVER_ROUNDTRIP_DOCUMENT; name = "GateServer") + @test OpenAPI.server(plan; framework = "HTTP") isa String + error = try + OpenAPI.server(plan; framework = :Nope) + nothing + catch caught + caught + end + @test error isa ArgumentError + @test occursin("HTTP", error.msg) + end + + @testset "multipart/mixed requests are rejected" begin + document = OpenAPI.obj( + "openapi" => "3.1.0", + "info" => OpenAPI.obj("title" => "Mixed", "version" => "1.0.0"), + "paths" => OpenAPI.obj( + "/mixed" => OpenAPI.obj( + "post" => OpenAPI.obj( + "operationId" => "mixed", + "requestBody" => OpenAPI.obj( + "content" => OpenAPI.obj( + "multipart/mixed" => OpenAPI.obj( + "schema" => OpenAPI.obj("type" => "object"), + ), + ), + ), + "responses" => OpenAPI.obj( + "204" => OpenAPI.obj("description" => "empty"), + ), + ), + ), + ), + ) + error = try + OpenAPI.serverplan(document) + nothing + catch caught + caught + end + @test error isa OpenAPI.OpenAPIError + @test any( + diagnostic -> diagnostic.code === :unsupported_multipart_server_generation, + error.diagnostics, + ) + @test OpenAPI.plan(document) isa OpenAPI.ClientPlan + end + + @testset "ambiguous exploded object parameters are rejected" begin + exploded(name) = OpenAPI.obj( + "name" => name, + "in" => "query", + "style" => "form", + "explode" => true, + "schema" => OpenAPI.obj( + "type" => "object", + "properties" => OpenAPI.obj("k" => OpenAPI.obj("type" => "string")), + ), + ) + document = OpenAPI.obj( + "openapi" => "3.1.0", + "info" => OpenAPI.obj("title" => "Ambiguous", "version" => "1.0.0"), + "paths" => OpenAPI.obj( + "/search" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "search", + "parameters" => Any[exploded("first"), exploded("second")], + "responses" => OpenAPI.obj( + "204" => OpenAPI.obj("description" => "empty"), + ), + ), + ), + ), + ) + error = try + OpenAPI.serverplan(document) + nothing + catch caught + caught + end + @test error isa OpenAPI.OpenAPIError + @test any( + diagnostic -> diagnostic.code === :ambiguous_exploded_object_parameters, + error.diagnostics, + ) + @test OpenAPI.plan(document) isa OpenAPI.ClientPlan + end +end From 1e359d6caeeafae83565b493a482595d0667fd13 Mon Sep 17 00:00:00 2001 From: Jacob Quinn Date: Fri, 7 Aug 2026 15:14:45 -0600 Subject: [PATCH 3/9] fix(client): accept zone-less ISO 8601 date-times as UTC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RFC 3339 requires an offset, but naive timestamps are what most JSON serializers print, so strict decoding rejected a large share of deployed APIs. Be liberal on input: a missing offset now means UTC — the same convention _encode already applies when it stamps naive DateTimes with Z. Malformed values and partial offsets still raise DecodeError. Co-Authored-By: Claude Fable 5 --- src/client.jl | 8 ++++++-- test/runtime.jl | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/client.jl b/src/client.jl index e9377bc..02e0d78 100644 --- a/src/client.jl +++ b/src/client.jl @@ -209,8 +209,12 @@ function _decode(::Type{Dates.Time}, value::AbstractString) end end function _decode(::Type{Dates.DateTime}, value::AbstractString) + # RFC 3339 requires the offset, but zone-less ISO 8601 date-times are + # widespread in deployed APIs (most JSON serializers print naive + # timestamps). Accept input liberally: a missing offset means UTC, the + # same convention _encode uses when it stamps naive DateTimes with `Z`. matched = match( - r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})$", + r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})?$", value, ) matched === nothing && throw(DecodeError("invalid RFC 3339 date-time $(repr(value))")) @@ -222,7 +226,7 @@ function _decode(::Type{Dates.DateTime}, value::AbstractString) output += Dates.Millisecond(milliseconds) end zone = matched.captures[3] - if zone != "Z" + if zone !== nothing && zone != "Z" sign = startswith(zone, '+') ? 1 : -1 hours = parse(Int, zone[2:3]) minutes = parse(Int, zone[5:6]) diff --git a/test/runtime.jl b/test/runtime.jl index 68651f7..6a409c1 100644 --- a/test/runtime.jl +++ b/test/runtime.jl @@ -37,6 +37,28 @@ @test invoke(:_path_parameter, "id", "a/b", :simple, false) == "a%2Fb" end + @testset "date-time decoding accepts RFC 3339 and zone-less ISO 8601" begin + decode(value) = invoke(:_decode, Dates.DateTime, value) + # canonical RFC 3339 forms + @test decode("2026-08-07T15:00:00Z") == Dates.DateTime(2026, 8, 7, 15) + @test decode("2026-08-07T15:00:00.076Z") == + Dates.DateTime(2026, 8, 7, 15, 0, 0, 76) + @test decode("2026-08-07T15:00:00+02:00") == + Dates.DateTime(2026, 8, 7, 13) + @test decode("2026-08-07T15:00:00-04:30") == + Dates.DateTime(2026, 8, 7, 19, 30) + # zone-less ISO 8601, as most JSON serializers print naive + # timestamps: interpreted as UTC, mirroring _encode's convention + @test decode("2026-08-07T15:00:00") == Dates.DateTime(2026, 8, 7, 15) + @test decode("2026-08-07T15:00:00.076") == + Dates.DateTime(2026, 8, 7, 15, 0, 0, 76) + # still not anything-goes + DecodeError = Base.invokelatest(getfield, C, :DecodeError) + @test_throws DecodeError decode("2026-08-07") + @test_throws DecodeError decode("2026-08-07T15:00:00+02") + @test_throws DecodeError decode("garbage") + end + @testset "query, header, and cookie styles" begin @test invoke(:_query_parameter, "q", ["a", "b"], :form, false, false) == [("q", "a,b", true)] From f7bf20b259155c84387383c73ef322aa6c637df6 Mon Sep 17 00:00:00 2001 From: Jacob Quinn Date: Sun, 9 Aug 2026 11:55:12 -0600 Subject: [PATCH 4/9] feat(client): lenient response decoding, zoned date-times, and streaming Address tanmaykm's production trial feedback on the rewrite: - An undocumented 2XX status no longer throws: an empty body returns nothing and a payload returns raw bytes. Undocumented error statuses still throw ApiError. - A response with no Content-Type decodes by status alone, as does a misreported Content-Type when only one media type is documented for the status. UnexpectedContentType is reserved for genuinely ambiguous multi-media responses. - A new datetime = :zoned generation option maps format: date-time to TimeZones.ZonedDateTime with offsets preserved end to end; the default Dates.DateTime mapping continues to normalize RFC 3339 offsets to UTC. - A new stream_to::Channel keyword on every generated operation streams response bodies incrementally over HTTP.open: consecutive JSON documents, JSON lines, RFC 7464 records, text lines, or raw chunks, each decoded to the documented response type. The call returns at the response head; closing the channel from the consumer aborts the transfer. Co-Authored-By: Claude Fable 5 --- AGENTS.md | 10 + README.md | 40 +++- src/client.jl | 439 ++++++++++++++++++++++++++++++++---- src/planning.jl | 33 ++- src/servergen.jl | 2 + test/Project.toml | 3 + test/runtests.jl | 2 +- test/runtime.jl | 101 +++++++++ test/runtime_integration.jl | 311 ++++++++++++++++++++++++- 9 files changed, 896 insertions(+), 45 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 85f9981..441bd99 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,6 +80,16 @@ The main files are: server planner additionally rejects non-form-data `multipart/*` request bodies and operations with more than one exploded object query or cookie parameter. +- Generation-time strictness does not extend to runtime tolerance of deployed + servers on success paths. Deliberately lenient client runtime behavior: + undocumented `2XX` statuses return `nothing` or raw bytes instead of + throwing, and a missing — or unambiguously misreported — response + Content-Type decodes by status alone. `UnexpectedContentType` is reserved + for genuinely ambiguous multi-media responses. +- Response streaming is a runtime feature (`stream_to::Channel` on every + generated operation, over `HTTP.open`), not an `itemSchema` planning + feature. Bodies split per media type: consecutive JSON documents, JSON + lines, RFC 7464 records, text lines, or raw chunks. ## Public types and functions diff --git a/README.md b/README.md index db9ac50..bd2f884 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,10 @@ Implemented model behavior includes: bounds, formats, and unevaluated constraints through runtime validation; - `readOnly` and `writeOnly` request and response projections; - Julia `Date`, `Time`, `DateTime`, `UUID`, and base64 byte values; +- `format: date-time` maps to `Dates.DateTime` by default, decoding RFC 3339 + offsets by normalizing to UTC; generate with `datetime = :zoned` to map to + `TimeZones.ZonedDateTime` instead, preserving offsets end to end (the + generated module then depends on TimeZones.jl); - deterministic names with protection against Julia keywords, Base/Core names, and generated runtime names. @@ -271,6 +275,38 @@ more than one representation. Use `request_headers` for one call and `Client(headers=...)` for all calls. `request_options` passes options to `HTTP.request`. +Responses are decoded by status alone when a server omits its Content-Type +header, or misreports it while only one media type is documented for that +status; `UnexpectedContentType` is thrown only when several documented media +types make the choice ambiguous. A `2XX` status the document does not describe +never fails the call: an empty body returns `nothing` and a payload returns +raw bytes. Undocumented error statuses still throw `ApiError`. + +## Streaming responses + +Pass `stream_to = Channel(n)` to any operation to consume the response body +incrementally, e.g. long-running watch endpoints or large exports: + +```julia +events = Channel{Any}(16) +ExampleClient.watch_pods(; client, stream_to = events) # returns at the response head +for event in events + # each item is decoded to the documented response type +end +``` + +The call returns as soon as the response head arrives (the channel itself, or +an `ApiResponse` whose body is the channel with `with_http_info = true`), and a +background task decodes items onto the channel. `application/json` bodies split +into consecutive JSON documents, each decoded against the documented response +schema — the convention used by Kubernetes-style watch endpoints. JSON Lines +and NDJSON bodies decode each line to the documented array's element type, and +JSON text sequences split on RFC 7464 record separators. `text/*` yields lines +and any other media type yields raw byte chunks. The channel closes when the +response ends, closes with the error when decoding or validation fails, and +closing it from the consumer side aborts the transfer. Error statuses still +throw `ApiError` with the fully buffered error body. + For a custom media type, register an encoder or decoder: ```julia @@ -330,7 +366,9 @@ client needs. The following boundaries are intentional and explicit: | OAS 3.2 streaming `itemSchema`, `itemEncoding`, and `prefixEncoding` | **Deferred. Client planning fails with `unsupported_streaming_generation`.** | The two deferred features fail during planning. They never produce a client -that silently sends the wrong wire format. +that silently sends the wrong wire format. Runtime response streaming with +`stream_to` is independent of the deferred OAS 3.2 `itemSchema` generation: it +streams response bodies that are described by normal schemas. `strict=true` is the default. Use `strict=false` only for documented ecosystem compatibility cases. Permissive mode can retain ambiguous path templates and a diff --git a/src/client.jl b/src/client.jl index 02e0d78..6a89a3b 100644 --- a/src/client.jl +++ b/src/client.jl @@ -576,6 +576,42 @@ function _encode_sequential_json(value, media) end """ +# Emitted only for `datetime = :zoned` plans, directly after +# GENERATED_RUNTIME_COMMON, in both generated clients and servers. +const GENERATED_ZONED_RUNTIME = raw""" +function _decode(::Type{TimeZones.ZonedDateTime}, value::AbstractString) + matched = match( + r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})?$", + value, + ) + matched === nothing && throw(DecodeError("invalid RFC 3339 date-time $(repr(value))")) + try + clock = Dates.DateTime(matched.captures[1]) + fraction = matched.captures[2] + if fraction !== nothing + clock += Dates.Millisecond( + parse(Int, rpad(first(fraction, min(3, length(fraction))), 3, '0')), + ) + end + zone = matched.captures[3] + timezone = if zone === nothing || zone == "Z" + TimeZones.tz"UTC" + else + sign = startswith(zone, '+') ? 1 : -1 + hours = parse(Int, zone[2:3]) + minutes = parse(Int, zone[5:6]) + TimeZones.FixedTimeZone(zone, sign * (3600 * hours + 60 * minutes)) + end + return TimeZones.ZonedDateTime(clock, timezone) + catch error + error isa DecodeError && rethrow() + throw(DecodeError("invalid RFC 3339 date-time: $(sprint(showerror, error))")) + end +end +_encode(value::TimeZones.ZonedDateTime) = + Dates.format(value, TimeZones.ISOZonedDateTimeFormat) +""" + const GENERATED_RUNTIME = raw""" struct ApiError <: Exception operation_id::String @@ -1853,6 +1889,7 @@ function _request( request_headers = Pair{String,String}[], request_options::NamedTuple = NamedTuple(), multipart_headers = NamedTuple(), + stream_to::Union{Nothing,Channel} = nothing, ) path = operation.path query = Tuple{String,String,Bool,Bool}[] @@ -1967,6 +2004,16 @@ function _request( selected_accept === nothing || isempty(selected_accept) || _set_header!(headers, "Accept", selected_accept) + stream_to === nothing || return _stream_request( + client, + operation, + url, + headers, + payload, + options, + stream_to, + with_http_info, + ) options = merge(options, (body = payload, status_exception = false)) response = HTTP.request( operation.method, @@ -1977,9 +2024,48 @@ function _request( response_headers = Pair{String,String}[ String(key) => String(value) for (key, value) in response.headers ] - bytes = Vector{UInt8}(response.body) - descriptor = _select_response(operation.responses, response.status) - received = HTTP.header(response, "Content-Type", "") + return _finish_buffered_response( + client, + operation, + Int(response.status), + response_headers, + HTTP.header(response, "Content-Type", ""), + Vector{UInt8}(response.body), + with_http_info, + ) +end + +# Deployed servers routinely omit or misreport Content-Type, so decode by +# status alone unless several documented media types make the choice ambiguous. +# Returns the selected media entry and the media type to decode the body as. +function _selected_media_entry(operation_id, status, descriptor, received) + selected = isempty(received) ? nothing : + _select_media(descriptor.media, received) + selected === nothing || return selected, String(received) + if isempty(received) || length(descriptor.media) == 1 + entry = first(descriptor.media) + return entry, entry[1] + end + throw( + UnexpectedContentType( + operation_id, + status, + received, + Tuple(first.(descriptor.media)), + ), + ) +end + +function _finish_buffered_response( + client::Client, + operation, + status::Int, + response_headers, + received, + bytes::Vector{UInt8}, + with_http_info::Bool, +) + descriptor = _select_response(operation.responses, status) decoded = nothing decoded_headers = Dict{String,Any}() decode_error = nothing @@ -1987,22 +2073,13 @@ function _request( decoded_headers = _decode_response_headers(client, descriptor, response_headers) if descriptor !== nothing if isempty(descriptor.media) - if !isempty(bytes) && 200 <= response.status < 300 - throw(UnexpectedBody(operation.id, response.status, length(bytes))) + if !isempty(bytes) && 200 <= status < 300 + throw(UnexpectedBody(operation.id, status, length(bytes))) end decoded = nothing else - selected = isempty(received) && isempty(bytes) ? first(descriptor.media) : - _select_media(descriptor.media, received) - selected === nothing && throw( - UnexpectedContentType( - operation.id, - response.status, - received, - Tuple(first.(descriptor.media)), - ), - ) - actual_media = isempty(received) ? selected[1] : received + selected, actual_media = + _selected_media_entry(operation.id, status, descriptor, received) decoded = _decode_body( client, selected[2], @@ -2011,41 +2088,319 @@ function _request( selected[3], ) end - elseif isempty(operation.responses) + else + # The response status is not documented. Specs regularly leave + # data-less statuses (204, redirects) out, so a successful call + # must not fail here: surface an empty body as `nothing` and any + # payload as raw bytes. decoded = isempty(bytes) ? nothing : copy(bytes) end catch error - 200 <= response.status < 300 && rethrow() + 200 <= status < 300 && rethrow() decode_error = error end - if !(200 <= response.status < 300) - throw( - ApiError( - operation.id, - response.status, - response_headers, - decoded_headers, - bytes, - decoded, - decode_error, - ), - ) - end - descriptor === nothing && !isempty(operation.responses) && throw( + 200 <= status < 300 || throw( ApiError( operation.id, - response.status, + status, response_headers, decoded_headers, bytes, - nothing, - nothing, - ) + decoded, + decode_error, + ), ) return with_http_info ? - ApiResponse(response.status, response_headers, decoded_headers, decoded) : + ApiResponse(status, response_headers, decoded_headers, decoded) : decoded end + +# ── streaming responses ────────────────────────────────────────────────────── +# +# Passing `stream_to::Channel` to an operation delivers the response body +# incrementally: the call returns as soon as the response head arrives, a +# background task splits the body into items, decodes each one, and `put!`s it +# on the channel. The channel is closed when the response ends, or closed with +# the error when splitting or decoding fails. Closing the channel from the +# consumer side aborts the transfer. + +# How the body is split into items, and what each item decodes to: +# `:json` re-uses the documented response schema per item (concatenated or +# newline-separated JSON documents, e.g. watch-style endpoints); sequential +# JSON media types split records and decode each to the documented array's +# element type; `text/*` yields lines; anything else yields raw byte chunks. +function _stream_plan(media, type, schema) + if _is_sequential_json_media(media) + item_type = type <: AbstractVector && type !== Vector{UInt8} ? + eltype(type) : type + kind = media == "application/json-seq" || endswith(media, "+json-seq") ? + :jsonseq : :jsonlines + # The documented schema describes the whole sequence, not one record, + # so per-item validation is skipped. + return kind, item_type, nothing + elseif _is_json_media(media) + return :json, type, schema + elseif startswith(media, "text/") || type === String + return :text, type, nothing + end + return :bytes, Vector{UInt8}, nothing +end + +_stream_whitespace(byte::UInt8) = + byte == 0x20 || byte == 0x09 || byte == 0x0d || byte == 0x0a + +function _next_stream_line!(buffer::Vector{UInt8}, final::Bool) + while !isempty(buffer) + index = findfirst(==(0x0a), buffer) + if index === nothing + final || return nothing + frame = copy(buffer) + empty!(buffer) + else + frame = buffer[1:index-1] + deleteat!(buffer, 1:index) + end + while !isempty(frame) && frame[end] == 0x0d + pop!(frame) + end + isempty(frame) || return frame + end + return nothing +end + +# Extract the next complete item from `buffer`, or return `nothing` when more +# bytes are needed. `final` marks the end of the response body. +function _next_stream_frame!(buffer::Vector{UInt8}, kind::Symbol, final::Bool) + if kind === :jsonseq + # RFC 7464: records start with RS and may contain unescaped newlines, + # so a record is complete at the next RS or at the end of the body. + while true + start = 1 + while start <= length(buffer) && + (buffer[start] == 0x1e || _stream_whitespace(buffer[start])) + start += 1 + end + start > 1 && deleteat!(buffer, 1:start-1) + isempty(buffer) && return nothing + index = findfirst(==(0x1e), buffer) + if index === nothing + final || return nothing + frame = copy(buffer) + empty!(buffer) + else + frame = buffer[1:index-1] + deleteat!(buffer, 1:index-1) + end + while !isempty(frame) && _stream_whitespace(frame[end]) + pop!(frame) + end + isempty(frame) || return frame + end + end + kind === :json || return _next_stream_line!(buffer, final) + start = 1 + while start <= length(buffer) && + (_stream_whitespace(buffer[start]) || buffer[start] == 0x1e) + start += 1 + end + start > 1 && deleteat!(buffer, 1:start-1) + isempty(buffer) && return nothing + open_byte = buffer[1] + if open_byte == UInt8('{') || open_byte == UInt8('[') + close_byte = open_byte == UInt8('{') ? UInt8('}') : UInt8(']') + depth = 0 + in_string = false + escaped = false + for (index, byte) in enumerate(buffer) + if escaped + escaped = false + elseif in_string + byte == UInt8('\\') && (escaped = true) + byte == UInt8('"') && (in_string = false) + elseif byte == UInt8('"') + in_string = true + elseif byte == open_byte + depth += 1 + elseif byte == close_byte + depth -= 1 + if depth == 0 + frame = buffer[1:index] + deleteat!(buffer, 1:index) + return frame + end + end + end + return nothing + end + # top-level scalar items (numbers, strings, booleans) end at a newline + return _next_stream_line!(buffer, final) +end + +function _decode_stream_item(client::Client, frame::Vector{UInt8}, kind::Symbol, item_type, schema) + if kind === :text + isvalid(String, frame) || + throw(DecodeError("streaming text response is not UTF-8")) + return _decode(item_type, String(frame)) + end + value = _parse_json(frame, "decoding a streaming response item") + schema === nothing || !client.validate_responses || _validate_schema( + schema, + value, + "decoding a streaming response item"; + direction = :output, + ) + return _decode(item_type, value) +end + +function _drain_stream_buffer!( + client::Client, + channel::Channel, + buffer::Vector{UInt8}, + kind::Symbol, + item_type, + schema, + final::Bool, +) + while true + frame = _next_stream_frame!(buffer, kind, final) + frame === nothing && break + put!(channel, _decode_stream_item(client, frame, kind, item_type, schema)) + end + final && kind === :json && !all(_stream_whitespace, buffer) && + throw(DecodeError("streaming response ended with a truncated item")) + return nothing +end + +function _pump_stream!( + client::Client, + stream, + channel::Channel, + kind::Symbol, + item_type, + schema, +) + buffer = UInt8[] + try + while !eof(stream) + chunk = readavailable(stream) + isempty(chunk) && continue + if kind === :bytes + put!(channel, chunk) + else + append!(buffer, chunk) + _drain_stream_buffer!( + client, + channel, + buffer, + kind, + item_type, + schema, + false, + ) + end + end + kind === :bytes || _drain_stream_buffer!( + client, + channel, + buffer, + kind, + item_type, + schema, + true, + ) + close(channel) + catch error + # A channel the consumer closed is the abort signal; anything else is + # delivered to the consumer through the channel. + error isa InvalidStateException && !isopen(channel) || close(channel, error) + finally + try + HTTP.closeread(stream) + catch + end + end + return nothing +end + +function _stream_request( + client::Client, + operation, + url, + headers, + payload::Vector{UInt8}, + options, + stream_to::Channel, + with_http_info::Bool, +) + stream = HTTP.open(operation.method, url, headers; options...) + local response + try + isempty(payload) || write(stream, payload) + HTTP.closewrite(stream) + response = HTTP.startread(stream) + catch + try + HTTP.closeread(stream) + catch + end + rethrow() + end + response_headers = Pair{String,String}[ + String(key) => String(value) for (key, value) in response.headers + ] + received = HTTP.header(response, "Content-Type", "") + status = Int(response.status) + if !(200 <= status < 300) + bytes = try + read(stream) + finally + try + HTTP.closeread(stream) + catch + end + end + # Throws ApiError with the fully decoded error body. + return _finish_buffered_response( + client, + operation, + status, + response_headers, + received, + bytes, + with_http_info, + ) + end + local descriptor, decoded_headers, kind, item_type, schema + try + descriptor = _select_response(operation.responses, status) + decoded_headers = _decode_response_headers(client, descriptor, response_headers) + if descriptor === nothing || isempty(descriptor.media) + kind, item_type, schema = :bytes, Vector{UInt8}, nothing + else + selected, actual_media = + _selected_media_entry(operation.id, status, descriptor, received) + kind, item_type, schema = + _stream_plan(_base_media_type(actual_media), selected[2], selected[3]) + end + catch + try + HTTP.closeread(stream) + catch + end + rethrow() + end + Threads.@spawn _pump_stream!( + $client, + $stream, + $stream_to, + $kind, + $item_type, + $schema, + ) + return with_http_info ? + ApiResponse(status, response_headers, decoded_headers, stream_to) : + stream_to +end """ function _julia_literal(value) @@ -3079,6 +3434,7 @@ function _emit_operation(io, operation::OperationPlan, const_name) "with_http_info::Bool = false", "request_headers = Pair{String,String}[]", "request_options::NamedTuple = NamedTuple()", + "stream_to::Union{Nothing,Channel} = nothing", ], ) summary = something(operation.operation.summary, operation.operation.description, "") @@ -3108,7 +3464,7 @@ function _emit_operation(io, operation::OperationPlan, const_name) const_name, ", values; body = ", body_expression, - ", content_type, accept, with_http_info, request_headers, request_options", + ", content_type, accept, with_http_info, request_headers, request_options, stream_to", ) has_multipart_request && print(io, ", multipart_headers") println(io, ")") @@ -3127,11 +3483,13 @@ function _generate(plan::ClientPlan) ) println(io, "module ", plan.module_name, "\n") println(io, "using HTTP, JSON, OpenAPI, Base64, Dates, UUIDs") + plan.datetime === :zoned && println(io, "using TimeZones") println(io, "const SchemaEngine = OpenAPI.SchemaEngine\n") _emit_security(io, plan) _emit_schema_data(io, plan) default_server = _default_server(plan.api) print(io, GENERATED_RUNTIME_COMMON, '\n') + plan.datetime === :zoned && print(io, GENERATED_ZONED_RUNTIME, '\n') print(io, GENERATED_RUNTIME, '\n') println(io, "const _DEFAULT_SERVER = ", repr(default_server)) println(io, "const SERVER = Ref{String}(_DEFAULT_SERVER)") @@ -3169,6 +3527,11 @@ Generate a deterministic Julia client module after full OpenAPI loading, reference binding, semantic normalization, and type planning. The generated module supports OpenAPI 3.0, 3.1, and 3.2 request/response models, parameter styles, content negotiation, and security requirements. + +`datetime = :utc` (the default) maps `format: date-time` to `Dates.DateTime` +and normalizes RFC 3339 offsets to UTC while decoding; `datetime = :zoned` +maps to `TimeZones.ZonedDateTime` and preserves offsets, making the generated +module depend on TimeZones.jl. """ function client( source; diff --git a/src/planning.jl b/src/planning.jl index 624b39e..3e6973d 100644 --- a/src/planning.jl +++ b/src/planning.jl @@ -69,6 +69,7 @@ struct ClientPlan uses_dates::Bool uses_uuids::Bool uses_base64::Bool + datetime::Symbol end struct ServerPlan @@ -80,6 +81,7 @@ struct ServerPlan uses_dates::Bool uses_uuids::Bool uses_base64::Bool + datetime::Symbol end const GenerationPlan = Union{ClientPlan,ServerPlan} @@ -115,6 +117,7 @@ mutable struct PlanningContext uses_dates::Bool uses_uuids::Bool uses_base64::Bool + datetime::Symbol end const JULIA_RESERVED_NAMES = Set([ @@ -445,7 +448,11 @@ function _base_component_names(api::NormalizedAPI) return output end -function PlanningContext(api::NormalizedAPI, bag::DiagnosticBag) +function PlanningContext( + api::NormalizedAPI, + bag::DiagnosticBag; + datetime::Symbol = :utc, +) return PlanningContext( api, bag, @@ -460,7 +467,18 @@ function PlanningContext(api::NormalizedAPI, bag::DiagnosticBag) false, false, false, + datetime, + ) +end + +function _validate_datetime_option(datetime::Symbol) + datetime in (:utc, :zoned) || throw( + ArgumentError( + "datetime must be :utc (Dates.DateTime, offsets normalized to UTC) " * + "or :zoned (TimeZones.ZonedDateTime, offsets preserved), got $(repr(datetime))", + ), ) + return datetime end function _model_name!(context::PlanningContext, view::SchemaView, suggested, mode) @@ -587,7 +605,8 @@ function _primitive_type!( return "Dates.Date" elseif format == "date-time" context.uses_dates = true - return "Dates.DateTime" + return context.datetime === :zoned ? "TimeZones.ZonedDateTime" : + "Dates.DateTime" elseif format == "time" context.uses_dates = true return "Dates.Time" @@ -1554,12 +1573,14 @@ function plan( name::AbstractString = "ApiClient", strict::Bool = true, max_diagnostics::Integer = 1_000, + datetime::Symbol = :utc, kwargs..., ) + _validate_datetime_option(datetime) api = source isa NormalizedAPI ? source : normalize(source; strict, max_diagnostics, kwargs...) bag = DiagnosticBag(max_diagnostics) - context = PlanningContext(api, bag) + context = PlanningContext(api, bag; datetime) _check_generation_support!(context, strict) operations = _plan_components!(context) diagnostics = _finish_diagnostics(context, "Cannot plan Julia client") @@ -1574,6 +1595,7 @@ function plan( context.uses_dates, context.uses_uuids, context.uses_base64, + context.datetime, ) end @@ -1633,12 +1655,14 @@ function serverplan( name::AbstractString = "ApiServer", strict::Bool = true, max_diagnostics::Integer = 1_000, + datetime::Symbol = :utc, kwargs..., ) + _validate_datetime_option(datetime) api = source isa NormalizedAPI ? source : normalize(source; strict, max_diagnostics, kwargs...) bag = DiagnosticBag(max_diagnostics) - context = PlanningContext(api, bag) + context = PlanningContext(api, bag; datetime) _check_generation_support!(context, strict) _check_server_generation_support!(context, strict) operations = _plan_components!(context) @@ -1652,5 +1676,6 @@ function serverplan( context.uses_dates, context.uses_uuids, context.uses_base64, + context.datetime, ) end diff --git a/src/servergen.jl b/src/servergen.jl index f1b850a..6dbeeaf 100644 --- a/src/servergen.jl +++ b/src/servergen.jl @@ -776,10 +776,12 @@ function server_module_source( end println(io, "module ", plan.module_name, "\n") println(io, imports) + plan.datetime === :zoned && println(io, "using TimeZones") println(io, "const SchemaEngine = OpenAPI.SchemaEngine\n") _emit_security(io, plan) _emit_schema_data(io, plan) print(io, GENERATED_RUNTIME_COMMON, '\n') + plan.datetime === :zoned && print(io, GENERATED_ZONED_RUNTIME, '\n') print(io, GENERATED_RUNTIME_SERVER, '\n') indices = _model_indices(plan) wrapped_aliases = _cyclic_aliases(plan) diff --git a/test/Project.toml b/test/Project.toml index 2f785c1..00faec2 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -4,9 +4,12 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +OpenAPI = "d5e62ea6-ddf3-4d43-8e4c-ad5e6c8bfd7d" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" +Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" ZipFile = "a5390f91-8eb1-5f08-bee0-b1d1ffed6cea" diff --git a/test/runtests.jl b/test/runtests.jl index f1dfd81..3e404cd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,5 @@ using Test, OpenAPI, JSON, Dates -import Downloads, HTTP +import Downloads, HTTP, Sockets, TimeZones const SchemaEngine = OpenAPI.SchemaEngine diff --git a/test/runtime.jl b/test/runtime.jl index 6a409c1..26663a1 100644 --- a/test/runtime.jl +++ b/test/runtime.jl @@ -59,6 +59,107 @@ @test_throws DecodeError decode("garbage") end + @testset "datetime = :zoned preserves time zone offsets" begin + zoned_document = OpenAPI.obj( + "openapi" => "3.2.0", + "info" => OpenAPI.obj("title" => "Zoned", "version" => "1"), + "paths" => OpenAPI.obj( + "/event" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "getEvent", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "event", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "\$ref" => "#/components/schemas/Event", + ), + ), + ), + ), + ), + ), + ), + ), + "components" => OpenAPI.obj( + "schemas" => OpenAPI.obj( + "Event" => OpenAPI.obj( + "type" => "object", + "required" => ["at"], + "properties" => OpenAPI.obj( + "at" => OpenAPI.obj( + "type" => "string", + "format" => "date-time", + ), + ), + "additionalProperties" => false, + ), + ), + ), + ) + @test_throws ArgumentError OpenAPI.client( + zoned_document; + name = "ZonedUnitClient", + datetime = :bogus, + ) + source = OpenAPI.client( + zoned_document; + name = "ZonedUnitClient", + datetime = :zoned, + ) + @test occursin("using TimeZones", source) + @test occursin("at::TimeZones.ZonedDateTime", source) + zoned_host = Module(:ZonedUnitClientHost) + Base.include_string(zoned_host, source, "ZonedUnitClient.jl") + Cz = Base.invokelatest(getfield, zoned_host, :ZonedUnitClient) + zoned_invoke(function_name, args...; kwargs...) = + Base.invokelatest(getfield(Cz, function_name), args...; kwargs...) + + decoded = zoned_invoke( + :_decode, + TimeZones.ZonedDateTime, + "2026-08-07T15:00:00.5+05:30", + ) + @test decoded == TimeZones.ZonedDateTime( + Dates.DateTime(2026, 8, 7, 15, 0, 0, 500), + TimeZones.tz"UTC+05:30", + ) + @test Dates.value(decoded.zone.offset) == 5 * 3600 + 30 * 60 + encoded = zoned_invoke(:_encode, decoded) + @test encoded == "2026-08-07T15:00:00.500+05:30" + @test zoned_invoke(:_decode, TimeZones.ZonedDateTime, encoded) == decoded + @test zoned_invoke(:_decode, TimeZones.ZonedDateTime, "2026-08-07T15:00:00Z") == + TimeZones.ZonedDateTime( + Dates.DateTime(2026, 8, 7, 15), + TimeZones.tz"UTC", + ) + @test zoned_invoke(:_decode, TimeZones.ZonedDateTime, "2026-08-07T15:00:00") == + TimeZones.ZonedDateTime( + Dates.DateTime(2026, 8, 7, 15), + TimeZones.tz"UTC", + ) + ZonedDecodeError = Base.invokelatest(getfield, Cz, :DecodeError) + @test_throws ZonedDecodeError zoned_invoke( + :_decode, + TimeZones.ZonedDateTime, + "2026-08-07T15:00:00+02", + ) + + event = zoned_invoke( + :_decode, + Base.invokelatest(getfield, Cz, :EventModel), + JSON.parse("""{"at":"2026-08-07T15:00:00-04:00"}"""), + ) + @test Dates.value(event.at.zone.offset) == -4 * 3600 + @test zoned_invoke(:_encode, event)["at"] == "2026-08-07T15:00:00.000-04:00" + + # the default mapping stays plain, trim-friendly Dates.DateTime + default_source = OpenAPI.client(zoned_document; name = "ZonedUnitClient") + @test !occursin("ZonedDateTime", default_source) + @test occursin("at::Dates.DateTime", default_source) + end + @testset "query, header, and cookie styles" begin @test invoke(:_query_parameter, "q", ["a", "b"], :form, false, false) == [("q", "a,b", true)] diff --git a/test/runtime_integration.jl b/test/runtime_integration.jl index 13baa22..9582894 100644 --- a/test/runtime_integration.jl +++ b/test/runtime_integration.jl @@ -93,6 +93,22 @@ end return HTTP.Response(200, ["Content-Type" => "text/plain"], "surprise") elseif startswith(path, "/wrong-content") return HTTP.Response(200, ["Content-Type" => "text/html"], "

wrong

") + elseif startswith(path, "/sloppy-content") + return HTTP.Response(200, ["Content-Type" => "text/plain"], """{"ok":true}""") + elseif startswith(path, "/ambiguous-content") + return HTTP.Response(200, ["Content-Type" => "application/xml"], "") + elseif startswith(path, "/no-content-type") + return HTTP.Response(200, Pair{String,String}[], """{"ok":true}""") + elseif startswith(path, "/undocumented/204") + return HTTP.Response(204) + elseif startswith(path, "/undocumented/201") + return HTTP.Response( + 201, + ["Content-Type" => "application/json"], + """{"extra":true}""", + ) + elseif startswith(path, "/undocumented/404") + return HTTP.Response(404, ["Content-Type" => "text/plain"], "missing") elseif startswith(path, "/bad-text") return HTTP.Response( 200, @@ -193,6 +209,15 @@ end ), "additionalProperties" => false, ) + event_schema = OpenAPI.obj( + "type" => "object", + "required" => ["kind", "seq"], + "properties" => OpenAPI.obj( + "kind" => string_schema, + "seq" => OpenAPI.obj("type" => "integer"), + ), + "additionalProperties" => false, + ) paths = OpenAPI.obj( "/styles/{simple}/{label}/{matrix}" => OpenAPI.obj( @@ -439,6 +464,55 @@ end ), ), ), + "/sloppy-content" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "sloppyContent", + "responses" => OpenAPI.obj( + "200" => runtime_response("application/json", OpenAPI.obj()), + ), + ), + ), + "/ambiguous-content" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "ambiguousContent", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "two media types", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj(), + ), + "text/plain" => OpenAPI.obj( + "schema" => string_schema, + ), + ), + ), + ), + ), + ), + "/no-content-type" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "missingContentType", + "responses" => OpenAPI.obj( + "200" => runtime_response("application/json", OpenAPI.obj()), + ), + ), + ), + "/undocumented/{code}" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "undocumentedStatus", + "parameters" => Any[ + runtime_parameter( + "code", + "path", + OpenAPI.obj("type" => "integer"), + ), + ], + "responses" => OpenAPI.obj( + "200" => runtime_response("application/json", OpenAPI.obj()), + ), + ), + ), "/bad-text" => OpenAPI.obj( "get" => OpenAPI.obj( "operationId" => "badText", @@ -461,6 +535,74 @@ end ), ), ), + "/stream/watch" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "watchStream", + "responses" => OpenAPI.obj( + "200" => runtime_response("application/json", event_schema), + ), + ), + ), + "/stream/logs" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "logStream", + "responses" => OpenAPI.obj( + "200" => runtime_response( + "application/x-ndjson", + OpenAPI.obj( + "type" => "array", + "items" => OpenAPI.obj("type" => "integer"), + ), + ), + ), + ), + ), + "/stream/seq" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "seqStream", + "responses" => OpenAPI.obj( + "200" => runtime_response( + "application/json-seq", + OpenAPI.obj( + "type" => "array", + "items" => string_schema, + ), + ), + ), + ), + ), + "/stream/truncated" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "truncatedStream", + "responses" => OpenAPI.obj( + "200" => runtime_response("application/json", event_schema), + ), + ), + ), + "/stream/invalid" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "invalidStream", + "responses" => OpenAPI.obj( + "200" => runtime_response("application/json", event_schema), + ), + ), + ), + "/stream/missing" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "missingStream", + "responses" => OpenAPI.obj( + "200" => runtime_response("application/json", event_schema), + ), + ), + ), + "/stream/forever" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "foreverStream", + "responses" => OpenAPI.obj( + "200" => runtime_response("application/json", event_schema), + ), + ), + ), "/custom" => OpenAPI.obj( "post" => OpenAPI.obj( "operationId" => "customCodec", @@ -787,10 +929,177 @@ end @test_throws C.UnexpectedBody call(:unexpectedbody; client) take_request() - @test_throws C.UnexpectedContentType call(:wrongcontent; client) + # A misreported Content-Type falls back to the only documented + # media type, so the html body fails JSON decoding rather than + # content-type selection. + @test_throws C.DecodeError call(:wrongcontent; client) + take_request() + sloppy = call(:sloppycontent; client) take_request() + @test sloppy["ok"] === true + @test_throws C.UnexpectedContentType call(:ambiguouscontent; client) + take_request() + missing_content_type = call(:missingcontenttype; client) + take_request() + @test missing_content_type["ok"] === true @test_throws C.DecodeError call(:badtext; client) take_request() + + @test call(:undocumentedstatus, 204; client) === nothing + take_request() + undocumented_bytes = call(:undocumentedstatus, 201; client) + take_request() + @test undocumented_bytes isa Vector{UInt8} + @test JSON.parse(String(copy(undocumented_bytes)))["extra"] === true + undocumented_error = @test_throws C.ApiError call( + :undocumentedstatus, + 404; + client, + request_options = (; retry = false), + ) + take_request() + @test undocumented_error.value.status == 404 + @test undocumented_error.value.body == Vector{UInt8}(codeunits("missing")) + end + + @testset "streaming responses" begin + # A raw chunked HTTP/1.1 server gives the tests exact control over + # how response bytes split across the wire. + raw_server = Sockets.listen(Sockets.IPv4("127.0.0.1"), 0) + raw_port = Sockets.getsockname(raw_server)[2] + raw_client = C.Client("http://127.0.0.1:$raw_port") + send_chunk = (socket, text) -> begin + write(socket, string(ncodeunits(text), base = 16), "\r\n", text, "\r\n") + flush(socket) + end + chunked_head = (media) -> + "HTTP/1.1 200 OK\r\nContent-Type: $media\r\nTransfer-Encoding: chunked\r\n\r\n" + finish_chunks = (socket) -> begin + write(socket, "0\r\n\r\n") + flush(socket) + end + accept_task = @async while isopen(raw_server) + socket = try + Sockets.accept(raw_server) + catch + break + end + @async try + request_line = readline(socket) + while !isempty(readline(socket)) + end + path = split(request_line, ' ')[2] + if startswith(path, "/stream/watch") + write(socket, chunked_head("application/json")) + flush(socket) + # one item split across two chunks, then two more items + send_chunk(socket, """{"kind":"ADDED",""") + sleep(0.05) + send_chunk(socket, """ "seq":1}\n{"kind":"MODIFIED","seq":2}\n""") + sleep(0.05) + send_chunk(socket, """{"kind":"DELETED","seq":3}""") + finish_chunks(socket) + elseif startswith(path, "/stream/logs") + write(socket, chunked_head("application/x-ndjson")) + flush(socket) + send_chunk(socket, "1\n2\n") + sleep(0.05) + send_chunk(socket, "3") + finish_chunks(socket) + elseif startswith(path, "/stream/seq") + write(socket, chunked_head("application/json-seq")) + flush(socket) + send_chunk(socket, "\x1e\"alpha\"\n\x1e\"be") + sleep(0.05) + send_chunk(socket, "ta\"\n") + finish_chunks(socket) + elseif startswith(path, "/stream/truncated") + write(socket, chunked_head("application/json")) + flush(socket) + send_chunk(socket, """{"kind":"ADDED","seq":1}\n{"kind":"MOD""") + finish_chunks(socket) + elseif startswith(path, "/stream/invalid") + write(socket, chunked_head("application/json")) + flush(socket) + send_chunk(socket, """{"kind":"ADDED","seq":"nope"}""") + finish_chunks(socket) + elseif startswith(path, "/stream/missing") + body = """{"error":"nope"}""" + write( + socket, + "HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\n" * + "Content-Length: $(ncodeunits(body))\r\n\r\n$body", + ) + flush(socket) + elseif startswith(path, "/stream/forever") + write(socket, chunked_head("application/json")) + flush(socket) + for tick in 1:1000 + send_chunk(socket, """{"kind":"TICK","seq":$tick}\n""") + sleep(0.02) + end + end + close(socket) + catch + close(socket) + end + end + try + watch_channel = Channel{Any}(16) + watch_info = call( + :watchstream; + client = raw_client, + stream_to = watch_channel, + with_http_info = true, + ) + @test watch_info.status == 200 + @test watch_info.body === watch_channel + watch_items = collect(watch_channel) + @test [(item.kind, item.seq) for item in watch_items] == + [("ADDED", 1), ("MODIFIED", 2), ("DELETED", 3)] + + log_channel = Channel{Any}(16) + @test call(:logstream; client = raw_client, stream_to = log_channel) === + log_channel + log_items = collect(log_channel) + @test log_items == [1, 2, 3] + @test all(item -> item isa Int64, log_items) + + seq_channel = Channel{Any}(16) + call(:seqstream; client = raw_client, stream_to = seq_channel) + @test collect(seq_channel) == ["alpha", "beta"] + + truncated_channel = Channel{Any}(16) + call(:truncatedstream; client = raw_client, stream_to = truncated_channel) + first_event = take!(truncated_channel) + @test (first_event.kind, first_event.seq) == ("ADDED", 1) + @test_throws C.DecodeError take!(truncated_channel) + + invalid_channel = Channel{Any}(16) + call(:invalidstream; client = raw_client, stream_to = invalid_channel) + @test_throws C.SchemaValidationError take!(invalid_channel) + + missing_channel = Channel{Any}(16) + missing_error = @test_throws C.ApiError call( + :missingstream; + client = raw_client, + stream_to = missing_channel, + ) + @test missing_error.value.status == 404 + @test JSON.parse(String(copy(missing_error.value.body)))["error"] == + "nope" + + # Closing the channel from the consumer side aborts the + # transfer; the call itself already returned at the response + # head, so an endless stream does not block anything. + forever_channel = Channel{Any}(2) + call(:foreverstream; client = raw_client, stream_to = forever_channel) + first_tick = take!(forever_channel) + @test first_tick.kind == "TICK" + close(forever_channel) + finally + close(raw_server) + end end @testset "security OR, AND, roles, and credential types" begin From ac0689dfb665f95f621add69bda1b8a1aad6bca8 Mon Sep 17 00:00:00 2001 From: Jacob Quinn Date: Sun, 9 Aug 2026 12:03:28 -0600 Subject: [PATCH 5/9] fix(normalize): keep parameterized media type keys distinct Content keys that differ only in parameters are separate entries, not case-insensitive duplicates: the Kubernetes OpenAPI v3 documents pair application/json with application/json;stream=watch on every list operation, and the duplicate check previously rejected the whole document. Compare the full lowercased key instead of the stripped base type. Co-Authored-By: Claude Fable 5 --- src/normalize.jl | 7 ++++--- test/semantics.jl | 30 +++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/normalize.jl b/src/normalize.jl index 3e41fd3..ea38921 100644 --- a/src/normalize.jl +++ b/src/normalize.jl @@ -1012,9 +1012,10 @@ function _normalize_content!(context::NormalizationContext, value, node) "content key $(repr(content_type)) is not a valid media type or media range", itemnode, ) - normalized_content_type = lowercase( - strip(first(split(String(content_type), ';'; limit = 2))), - ) + # Keys differing only in parameters are distinct entries — deployed + # specs use them (Kubernetes documents `application/json` next to + # `application/json;stream=watch`) — so compare the whole key. + normalized_content_type = lowercase(strip(String(content_type))) normalized_content_type in seen && _reference_error!( context.resolver, :duplicate_media_type, diff --git a/test/semantics.jl b/test/semantics.jl index e757673..19e642f 100644 --- a/test/semantics.jl +++ b/test/semantics.jl @@ -235,7 +235,7 @@ end "description" => "media", "content" => OpenAPI.obj( "application/json" => OpenAPI.obj(), - "Application/JSON; charset=utf-8" => OpenAPI.obj(), + "Application/JSON" => OpenAPI.obj(), "not a media type" => OpenAPI.obj(), ), ), @@ -248,6 +248,34 @@ end media_codes = Set(diagnostic.code for diagnostic in media_error.value.diagnostics) @test :duplicate_media_type in media_codes @test :invalid_media_type in media_codes + + # Content keys differing only in parameters are distinct entries: the + # Kubernetes OpenAPI v3 documents pair `application/json` with + # `application/json;stream=watch` on every list operation. + parameterized = minimal_openapi( + "3.1.1", + OpenAPI.obj( + "/media" => OpenAPI.obj( + "get" => semantic_operation( + "media"; + responses = OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "media", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj(), + "application/json;stream=watch" => OpenAPI.obj(), + ), + ), + ), + ), + ), + ), + ) + normalized_parameterized = OpenAPI.normalize(parameterized) + parameterized_response = + only(only(normalized_parameterized.operations).responses) + @test [media.content_type for media in parameterized_response.content] == + ["application/json", "application/json;stream=watch"] end @testset "planning rejects incompatible styles and encodings" begin From bd96d53582d775fe07f86da5707f2cf471fad998 Mon Sep 17 00:00:00 2001 From: Jacob Quinn Date: Sun, 9 Aug 2026 17:36:41 -0600 Subject: [PATCH 6/9] fix(release): harden OpenAPI 1.0 proposal Quote specification-derived source and reserve generated identifiers. Make streaming cancellation deterministic and preserve normal protocol selection. Decode server form and multipart values by schema, preserve raw request bodies, and follow documented success responses. Add licensing, public API, CI, documentation, and regression coverage. --- .github/workflows/CI.yml | 2 + .github/workflows/TagBot.yml | 14 +- LICENSE.md | 4 + README.md | 25 +++- ext/OpenAPIHTTPExt.jl | 28 +++- schemas/LICENSE | 201 ++++++++++++++++++++++++++++ schemas/README.md | 3 +- src/OpenAPI.jl | 37 +++++- src/client.jl | 245 ++++++++++++++++++++++++++--------- src/document.jl | 4 +- src/planning.jl | 74 +++++++++-- src/servergen.jl | 79 ++++++++--- test/discriminators.jl | 95 ++++++++++++++ test/runtests.jl | 20 +++ test/runtime.jl | 34 +++++ test/runtime_integration.jl | 15 ++- test/servergen.jl | 204 ++++++++++++++++++++++++++--- 17 files changed, 941 insertions(+), 143 deletions(-) create mode 100644 schemas/LICENSE diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 7550e8a..a8da8e8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -11,6 +11,8 @@ jobs: test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} runs-on: ${{ matrix.os }} + env: + OPENAPI_SCHEMA_SUITE: ${{ matrix.version == '1' && 'all' || '' }} strategy: fail-fast: false matrix: diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml index fd05b28..f49313b 100644 --- a/.github/workflows/TagBot.yml +++ b/.github/workflows/TagBot.yml @@ -8,20 +8,8 @@ jobs: TagBot: if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' runs-on: ubuntu-latest - permissions: - actions: read - checks: read - contents: write - deployments: read - issues: read - discussions: read - packages: read - pages: read - pull-requests: read - repository-projects: read - security-events: read - statuses: read steps: - uses: JuliaRegistries/TagBot@v1 with: token: ${{ secrets.GITHUB_TOKEN }} + ssh: ${{ secrets.DOCUMENTER_KEY }} diff --git a/LICENSE.md b/LICENSE.md index 5ba278f..36ed9d3 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -2,6 +2,10 @@ The OpenAPI.jl package is licensed under the MIT "Expat" License: > Copyright (c) 2026: Jacob Quinn, JuliaServices contributors. > +> Copyright (c) 2026: fredo-dedup, quinnj, and contributors (schema engine). +> +> Copyright (c) 2022: Julia Computing Inc. All rights reserved. +> > Permission is hereby granted, free of charge, to any person obtaining > a copy of this software and associated documentation files (the > "Software"), to deal in the Software without restriction, including diff --git a/README.md b/README.md index bd2f884..6b34169 100644 --- a/README.md +++ b/README.md @@ -117,10 +117,10 @@ function get_widget(request, id; verbose = false) return lookup_widget(id; verbose) # encoded, validated, 200 end -# DELETE /widgets/{id} -> nothing becomes a 204 +# This operation documents 204, so nothing becomes an empty 204 response. delete_widget(request, id) = nothing -# Return an HTTP.Response directly for anything custom. +# Return an HTTP.Response directly for custom behavior. create_widget(request, body) = HTTP.Response(409, "already exists") end @@ -145,7 +145,16 @@ cookie parameters, JSON, `application/x-www-form-urlencoded`, and before handlers run. Decoding failures produce structured JSON `400` (or `415` for undocumented media types) responses without invoking the handler. Response values are validated against the output-direction schema and encoded from the -first documented success response. +first documented success response. Returning `nothing` follows that response: +it emits an empty body when the response has no content, or JSON `null` when +the selected JSON schema accepts null. A full `HTTP.Response` bypasses generated +status, header, and body validation. The handler owns that validation. + +Generated server stubs do not authenticate or authorize requests. Apply a +`middleware` that enforces the operation's security policy before it calls the +handler. `serverplan` rejects request-body Encoding Objects that the generated +HTTP server cannot recover faithfully. This includes nested encodings, custom +multipart part headers, and explicit body encoding style modifiers. ## Pipeline and diagnostics @@ -272,8 +281,11 @@ Generated clients support: Use `content_type=...` and `accept=...` on an operation when the document offers more than one representation. Use `request_headers` for one call and -`Client(headers=...)` for all calls. `request_options` passes options to -`HTTP.request`. +`Client(headers=...)` for all calls. `request_options` passes options to the +HTTP transport. Streaming calls default to HTTP/1.1 because consumer-driven +stream cancellation closes one request connection. Set `protocol=:auto` or +`:h2` in `request_options` when the caller accepts HTTP/2 stream lifecycle +semantics. Buffered calls keep HTTP.jl's automatic protocol selection. Responses are decoded by status alone when a server omits its Content-Type header, or misreports it while only one media type is documented for that @@ -453,4 +465,5 @@ OpenAPI behavior follows the normative [OpenAPI 3.1.1](https://spec.openapis.org/oas/v3.1.1.html), and [OpenAPI 3.2.0](https://spec.openapis.org/oas/v3.2.0.html) specifications. The files in `schemas/` are official structural schemas. The normative text -remains authoritative when a published schema differs from it. +remains authoritative when a published schema differs from it. Their Apache +License 2.0 text is included in [`schemas/LICENSE`](schemas/LICENSE). diff --git a/ext/OpenAPIHTTPExt.jl b/ext/OpenAPIHTTPExt.jl index 14f6d8b..ca4e0e9 100644 --- a/ext/OpenAPIHTTPExt.jl +++ b/ext/OpenAPIHTTPExt.jl @@ -6,8 +6,26 @@ module OpenAPIHTTPExt using OpenAPI, HTTP +struct _ResponseTooLarge <: Exception end + +mutable struct _BoundedResponseBuffer <: IO + buffer::IOBuffer + max_bytes::Int +end + +Base.isopen(buffer::_BoundedResponseBuffer) = isopen(buffer.buffer) +Base.position(buffer::_BoundedResponseBuffer) = position(buffer.buffer) +function Base.unsafe_write( + buffer::_BoundedResponseBuffer, + pointer::Ptr{UInt8}, + bytes::UInt, +) + bytes <= buffer.max_bytes - position(buffer) || throw(_ResponseTooLarge()) + return Base.unsafe_write(buffer.buffer, pointer, bytes) +end + function OpenAPI.fetchresource(id::OpenAPI.Resources.ResourceId, max_bytes::Integer) - body = Vector{UInt8}(undef, max_bytes) + body = _BoundedResponseBuffer(IOBuffer(), Int(max_bytes)) response = try HTTP.get( string(id); @@ -21,8 +39,7 @@ function OpenAPI.fetchresource(id::OpenAPI.Resources.ResourceId, max_bytes::Inte ) catch error message = sprint(showerror, error) - if occursin("response stream", message) || - occursin("DecompressionLimitError", message) + if error isa _ResponseTooLarge || error isa HTTP.DecompressionLimitError throw( OpenAPI.Resources.RetrievalError( id, @@ -43,7 +60,7 @@ function OpenAPI.fetchresource(id::OpenAPI.Resources.ResourceId, max_bytes::Inte "HTTP request returned status $(response.status)", ), ) - bytes = copy(body) + bytes = take!(body.buffer) media_type = HTTP.header(response, "Content-Type", "") return OpenAPI.Resources.RetrievedResource( id, @@ -96,7 +113,8 @@ function _http_handler(impl, entry) return function (request::HTTP.Request) content_values = _header_values(request.headers, "Content-Type") content_type = isempty(content_values) ? "" : first(content_values) - bytes = Vector{UInt8}(codeunits(String(request.body))) + bytes = request.body isa AbstractVector{UInt8} ? copy(request.body) : + Vector{UInt8}(codeunits(String(request.body))) local args, kwargs try parts = _http_multipart_parts(content_type, bytes) diff --git a/schemas/LICENSE b/schemas/LICENSE new file mode 100644 index 0000000..23b34fd --- /dev/null +++ b/schemas/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright The Linux Foundation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/schemas/README.md b/schemas/README.md index de3907d..9f402bf 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -10,5 +10,4 @@ schema. - `oas-3.2.json`: The schemas are distributed under the Apache License 2.0 as part of the -OpenAPI Specification project. See -. +OpenAPI Specification project. See [`LICENSE`](LICENSE). diff --git a/src/OpenAPI.jl b/src/OpenAPI.jl index 9f882c7..575ce88 100644 --- a/src/OpenAPI.jl +++ b/src/OpenAPI.jl @@ -2,7 +2,7 @@ OpenAPI.jl: build OpenAPI documents from declared endpoints, and generate Julia clients from OpenAPI documents. -Two pieces: +Three pieces: 1. **Document generation** — describe endpoints as [`OpenAPI.Operation`](@ref)s and get a valid OpenAPI 3.2.0 document. Framework packages can add router @@ -28,8 +28,6 @@ const OPENAPI_VERSION = "3.2.0" include("schema_engine/SchemaEngine.jl") -public SchemaEngine - include("schemas.jl") include("document.jl") include("diagnostics.jl") @@ -60,4 +58,37 @@ function operations end function fetchurl end function fetchresource end +public ClientPlan, + Diagnostic, + DocumentVersion, + NormalizedAPI, + OpenAPIError, + Operation, + Param, + Resources, + SchemaEngine, + SchemaRegistry, + ServerPlan, + SourceDocument, + SourceLocation, + SourcePosition, + check, + client, + document, + load, + location, + normalize, + oas_family, + obj, + operations, + parse, + plan, + read, + register!, + schemaof, + server, + server_source, + serverplan, + validate + end # module diff --git a/src/client.jl b/src/client.jl index 6a89a3b..1876945 100644 --- a/src/client.jl +++ b/src/client.jl @@ -370,11 +370,74 @@ function _set_header!(headers, name, value) return headers end +function _media_segments(value) + output = String[] + io = IOBuffer() + quoted = false + escaped = false + for char in String(value) + if escaped + write(io, char) + escaped = false + elseif quoted && char == '\\' + write(io, char) + escaped = true + elseif char == '"' + write(io, char) + quoted = !quoted + elseif char == ';' && !quoted + push!(output, String(take!(io))) + else + write(io, char) + end + end + push!(output, String(take!(io))) + return output +end + +function _media_parameter_value(value) + text = strip(String(value)) + startswith(text, '"') && endswith(text, '"') && ncodeunits(text) >= 2 || + return text + ncodeunits(text) == 2 && return "" + io = IOBuffer() + escaped = false + for char in SubString(text, nextind(text, firstindex(text)), prevind(text, lastindex(text))) + if escaped + write(io, char) + escaped = false + elseif char == '\\' + escaped = true + else + write(io, char) + end + end + escaped && write(io, '\\') + return String(take!(io)) +end + +function _media_type(value) + segments = _media_segments(value) + base = lowercase(strip(first(segments))) + parameters = Dict{String,String}() + for segment in Iterators.drop(segments, 1) + pair = split(segment, '='; limit = 2) + length(pair) == 2 || continue + name = lowercase(strip(pair[1])) + isempty(name) && continue + parameter = _media_parameter_value(pair[2]) + parameters[name] = name == "charset" ? lowercase(parameter) : parameter + end + return base, parameters +end + function _media_match_score(received::String, documented::String) - received == documented && return 4 - documented == "*/*" && return 1 - parts = split(documented, '/'; limit = 2) - received_parts = split(received, '/'; limit = 2) + received_base = first(_media_type(received)) + documented_base = first(_media_type(documented)) + received_base == documented_base && return 4 + documented_base == "*/*" && return 1 + parts = split(documented_base, '/'; limit = 2) + received_parts = split(received_base, '/'; limit = 2) length(parts) == 2 && length(received_parts) == 2 || return 0 parts[1] == received_parts[1] || parts[1] == "*" || return 0 parts[2] == "*" && return parts[1] == "*" ? 1 : 2 @@ -383,18 +446,29 @@ function _media_match_score(received::String, documented::String) return 0 end +function _media_selection_score(received::String, documented::String) + received_base, received_parameters = _media_type(received) + documented_base, documented_parameters = _media_type(documented) + base_score = _media_match_score(received_base, documented_base) + base_score > 0 || return (0, 0, 0) + all(documented_parameters) do (name, value) + return get(received_parameters, name, nothing) == value + end || return (0, 0, 0) + exact = length(received_parameters) == length(documented_parameters) + return (base_score, length(documented_parameters), exact ? 1 : 0) +end + _media_match(received::String, documented::String) = - _media_match_score(_base_media_type(received), _base_media_type(documented)) > 0 + first(_media_selection_score(received, documented)) > 0 _base_media_type(value) = lowercase(strip(first(split(String(value), ';'; limit = 2)))) function _select_media(media, received) isempty(media) && return nothing - normalized = _base_media_type(received) selected = nothing - score = 0 + score = (0, 0, 0) for entry in media - candidate = _media_match_score(normalized, _base_media_type(entry[1])) + candidate = _media_selection_score(String(received), String(entry[1])) if candidate > score selected = entry score = candidate @@ -1923,7 +1997,8 @@ function _request( end occursin('{', path) && throw(ArgumentError("not all path template parameters were supplied for $(operation.id)")) - base_options = merge(client.request_options, request_options) + default_options = stream_to isa Channel ? (; protocol = :h1) : NamedTuple() + base_options = merge(default_options, client.request_options, request_options) options = _security!( client, operation.security, @@ -1964,12 +2039,11 @@ function _request( payload = UInt8[] if operation.request !== nothing && !(body isa Absent) media = operation.request.media - selected = content_type === nothing ? first(media) : - something(findfirst(entry -> _media_match(lowercase(content_type), lowercase(entry[1])), media), 0) - selected === 0 && throw( + entry = content_type === nothing ? first(media) : + _select_media(media, String(content_type)) + entry === nothing && throw( ArgumentError("unsupported request Content-Type $(repr(content_type)) for $(operation.id)"), ) - entry = selected isa Integer ? media[selected] : selected client.validate_requests && _validate_schema( entry[3], _encode(body), @@ -2278,6 +2352,7 @@ function _pump_stream!( kind::Symbol, item_type, schema, + finished, ) buffer = UInt8[] try @@ -2308,16 +2383,41 @@ function _pump_stream!( schema, true, ) + finished[] = true close(channel) catch error # A channel the consumer closed is the abort signal; anything else is # delivered to the consumer through the channel. - error isa InvalidStateException && !isopen(channel) || close(channel, error) + if isopen(channel) + finished[] = true + close(channel, error) + end finally try HTTP.closeread(stream) catch end + finished[] = true + end + return nothing +end + +function _abort_stream_on_close!(stream, channel::Channel, producer::Task, finished) + while isopen(channel) && !finished[] + sleep(0.25) + end + finished[] && return nothing + try + HTTP.closeread(stream) + catch + end + for _ in 1:4 + istaskdone(producer) && return nothing + sleep(0.25) + end + try + close(stream) + catch end return nothing end @@ -2389,14 +2489,17 @@ function _stream_request( end rethrow() end - Threads.@spawn _pump_stream!( - $client, - $stream, - $stream_to, - $kind, - $item_type, - $schema, - ) + finished = Ref(false) + producer = errormonitor(@async _pump_stream!( + client, + stream, + stream_to, + kind, + item_type, + schema, + finished, + )) + errormonitor(@async _abort_stream_on_close!(stream, stream_to, producer, finished)) return with_http_info ? ApiResponse(status, response_headers, decoded_headers, stream_to) : stream_to @@ -2774,19 +2877,20 @@ function _emit_model( model.name, ", value)", ) - println(io, "function _decode(::Type{", model.name, "}, raw)") - println(io, " _validate_schema(", schema, ", raw, ", repr("decoding " * model.name), "; direction = ", direction, ")") - println(io, " value = _object(raw, ", repr(model.name), ")") + println(io, "function _decode(::Type{", model.name, "}, _openapi_raw)") + println(io, " _validate_schema(", schema, ", _openapi_raw, ", repr("decoding " * model.name), "; direction = ", direction, ")") + println(io, " _openapi_object = _object(_openapi_raw, ", repr(model.name), ")") for field in model.fields type = _rewrite_forward(field.type, index, indices, abstract_targets) + local_name = "_openapi_field_" * field.name if field.required println( io, " ", - field.name, + local_name, " = _decode(", type, - ", _required(value, ", + ", _required(_openapi_object, ", repr(field.wire_name), ", ", repr(model.name), @@ -2796,12 +2900,12 @@ function _emit_model( println( io, " ", - field.name, - " = haskey(value, ", + local_name, + " = haskey(_openapi_object, ", repr(field.wire_name), ") ? _decode(", type, - ", value[", + ", _openapi_object[", repr(field.wire_name), "]) : ABSENT", ) @@ -2810,51 +2914,53 @@ function _emit_model( known = Tuple(field.wire_name for field in model.fields) if model.additional_type !== nothing additional_type = _rewrite_forward(model.additional_type, index, indices, abstract_targets) - println(io, " additional_properties = Dict{String,", additional_type, "}()") - println(io, " for (key, item) in value") - println(io, " String(key) in ", _julia_literal(known), " && continue") - println(io, " additional_properties[String(key)] = _decode(", additional_type, ", item)") + println(io, " _openapi_additional_properties = Dict{String,", additional_type, "}()") + println(io, " for (_openapi_key, _openapi_item) in _openapi_object") + println(io, " String(_openapi_key) in ", _julia_literal(known), " && continue") + println(io, " _openapi_additional_properties[String(_openapi_key)] = _decode(", additional_type, ", _openapi_item)") println(io, " end") else - println(io, " unknown = setdiff(String.(collect(keys(value))), collect(", _julia_literal(known), "))") - println(io, " isempty(unknown) || throw(DecodeError(\"unknown fields while decoding ", model.name, ": \" * join(unknown, \", \")))") + println(io, " _openapi_unknown = setdiff(String.(collect(keys(_openapi_object))), collect(", _julia_literal(known), "))") + println(io, " isempty(_openapi_unknown) || throw(DecodeError(\"unknown fields while decoding ", model.name, ": \" * join(_openapi_unknown, \", \")))") end print(io, " return ", model.name, "(") - assignments = String[string(field.name, " = ", field.name) for field in model.fields] - model.additional_type === nothing || push!(assignments, "additional_properties = additional_properties") + assignments = String[ + string(field.name, " = _openapi_field_", field.name) for field in model.fields + ] + model.additional_type === nothing || push!(assignments, "additional_properties = _openapi_additional_properties") println(io, "; ", join(assignments, ", "), ")") println(io, "end") - println(io, "function _encode(value::", model.name, ")") - println(io, " output = JSON.Object{String,Any}()") + println(io, "function _encode(_openapi_value::", model.name, ")") + println(io, " _openapi_output = JSON.Object{String,Any}()") for field in model.fields - println(io, " value.", field.name, " isa Absent || (output[", repr(field.wire_name), "] = _encode(value.", field.name, "))") + println(io, " _openapi_value.", field.name, " isa Absent || (_openapi_output[", repr(field.wire_name), "] = _encode(_openapi_value.", field.name, "))") end if model.additional_type !== nothing - println(io, " for (key, item) in value.additional_properties") - println(io, " haskey(output, key) && throw(ArgumentError(\"additional property conflicts with declared field: \" * key))") - println(io, " output[key] = _encode(item)") + println(io, " for (_openapi_key, _openapi_item) in _openapi_value.additional_properties") + println(io, " haskey(_openapi_output, _openapi_key) && throw(ArgumentError(\"additional property conflicts with declared field: \" * _openapi_key))") + println(io, " _openapi_output[_openapi_key] = _encode(_openapi_item)") println(io, " end") end - println(io, " return _validate_schema(", schema, ", output, ", repr("encoding " * model.name), "; direction = ", direction, ")") + println(io, " return _validate_schema(", schema, ", _openapi_output, ", repr("encoding " * model.name), "; direction = ", direction, ")") println(io, "end\n") - println(io, "function _form_fields(value::", model.name, ")") - println(io, " output = Pair{String,Any}[]") + println(io, "function _form_fields(_openapi_value::", model.name, ")") + println(io, " _openapi_output = Pair{String,Any}[]") for field in model.fields println( io, - " value.", + " _openapi_value.", field.name, - " isa Absent || push!(output, ", + " isa Absent || push!(_openapi_output, ", repr(field.wire_name), - " => value.", + " => _openapi_value.", field.name, ")", ) end if model.additional_type !== nothing - println(io, " append!(output, collect(value.additional_properties))") + println(io, " append!(_openapi_output, collect(_openapi_value.additional_properties))") end - println(io, " return output") + println(io, " return _openapi_output") println(io, "end\n") end @@ -3164,6 +3270,17 @@ function _encoding_content_base(content_type) return lowercase(strip(first(split(selected, ';'; limit = 2)))) end +function _field_wire_kind(view::SchemaView) + format = _primitive_format(view) + format in ("byte", "base64", "binary") && return :bytes + _is_object_schema(view) && return :object + types = unique(_without_null_type(_effective_types(view))) + length(types) == 1 || return :default + type = only(types) + type in ("string", "boolean", "integer", "number") || return :default + return Symbol(type) +end + function _encoding_descriptor( encoding::NormalizedEncoding, base_media_type::String, @@ -3244,7 +3361,11 @@ function _media_descriptor(media, normalized) startswith(base_media_type, "multipart/") field_items = String[ "(name = " * repr(field_name) * - ", shape = " * repr(_schema_shape(field_view)) * ")" for + ", shape = " * repr(_schema_shape(field_view)) * + ", kind = " * repr(_field_wire_kind(field_view)) * + ", item_kind = " * repr( + _field_wire_kind(_named_encoding_value_view(field_view)), + ) * ")" for (field_name, field_view) in sort(collect(properties); by = first) ] fields = "(" * join(field_items, ',') * @@ -3438,11 +3559,11 @@ function _emit_operation(io, operation::OperationPlan, const_name) ], ) summary = something(operation.operation.summary, operation.operation.description, "") - println(io, "\"\"\"") - println(io, " ", operation.name, "(...)") - isempty(summary) || println(io, "\n", summary) - println(io, "\n`", operation.operation.method, " ", operation.operation.path, "`") - println(io, "\"\"\"") + doc = " " * operation.name * "(...)\n" + isempty(summary) || (doc *= "\n" * summary * "\n") + doc *= "\n`" * String(operation.operation.method) * " " * + operation.operation.path * "`" + println(io, "@doc ", repr(doc)) println( io, "function ", @@ -3453,16 +3574,16 @@ function _emit_operation(io, operation::OperationPlan, const_name) join(keywords, ", "), ")", ) - println(io, " values = Dict{Symbol,Any}()") + println(io, " _openapi_values = Dict{Symbol,Any}()") for parameter in operation.parameters - println(io, " values[", repr(Symbol(parameter.name)), "] = ", parameter.name) + println(io, " _openapi_values[", repr(Symbol(parameter.name)), "] = ", parameter.name) end body_expression = operation.request_body === nothing ? "ABSENT" : "body" print( io, " return _request(client, ", const_name, - ", values; body = ", + ", _openapi_values; body = ", body_expression, ", content_type, accept, with_http_info, request_headers, request_options, stream_to", ) @@ -3478,7 +3599,7 @@ function _generate(plan::ClientPlan) "# Generated by OpenAPI.jl from ", repr(plan.api.title), " version ", - plan.api.api_version, + repr(plan.api.api_version), ". Do not edit.", ) println(io, "module ", plan.module_name, "\n") diff --git a/src/document.jl b/src/document.jl index 5420fc5..26d7197 100644 --- a/src/document.jl +++ b/src/document.jl @@ -46,8 +46,8 @@ Keywords: - `summary=""` — human description - `params=Param[]` — path/query parameters - `bodytype=nothing` — Julia type of the request body, or `nothing` for none -- `responsetype=nothing` — Julia type of the success response; `nothing` means - unknown (empty schema), the *type* `Nothing` means 204 No Content, and a +- `responsetype=nothing` — no success response body (status 200); the *type* + `Nothing` means 204 No Content, `Any` means unconstrained JSON, and a `Union{Nothing, T}` emits both 204 and a 200 with `T`'s schema - `contenttype="application/json"` — media type for body/response content - `secured=false` — whether the operation requires authentication (emitted as a diff --git a/src/planning.jl b/src/planning.jl index 3e6973d..2d4aeff 100644 --- a/src/planning.jl +++ b/src/planning.jl @@ -188,6 +188,19 @@ const GENERATED_TYPE_NAMES = Set([ "Client", ]) +const GENERATED_PARAMETER_NAMES = Set([ + "body", + "client", + "content_type", + "accept", + "server", + "with_http_info", + "request_headers", + "request_options", + "multipart_headers", + "stream_to", +]) + function _words(value::AbstractString) normalized = replace(String(value), r"[^A-Za-z0-9]+" => " ") return [word for word in split(normalized) if !isempty(word)] @@ -216,6 +229,13 @@ function _field_identifier(value::AbstractString) return output end +function _operation_identifier(value::AbstractString) + output = _field_identifier(value) + symbol = Symbol(output) + (isdefined(Core, symbol) || isdefined(Base, symbol)) && (output *= '_') + return output +end + function _allocate_name!(context::PlanningContext, suggested::AbstractString) base = _type_identifier(suggested) count = get(context.used_names, base, 0) + 1 @@ -1227,17 +1247,7 @@ end function _plan_parameters!(context, operation, function_name) output = ParameterPlan[] - used = Dict( - "body" => 1, - "client" => 1, - "content_type" => 1, - "accept" => 1, - "server" => 1, - "with_http_info" => 1, - "request_headers" => 1, - "request_options" => 1, - "multipart_headers" => 1, - ) + used = Dict(name => 1 for name in GENERATED_PARAMETER_NAMES) for parameter in operation.parameters name = _field_identifier(parameter.name) count = get(used, name, 0) + 1 @@ -1360,7 +1370,7 @@ function _plan_responses!(context, operation, function_name) end function _plan_operation!(context, operation, used_functions) - base = _field_identifier(operation.id) + base = _operation_identifier(operation.id) count = get(used_functions, base, 0) + 1 used_functions[base] = count name = count == 1 ? base : string(base, '_', count) @@ -1637,6 +1647,46 @@ function _check_server_generation_support!(context::PlanningContext, strict::Boo ), ) end + for encoding in media.encoding + if !isempty(encoding.encoding) + _error!( + context.bag, + :unsupported_nested_server_encoding, + "server generation cannot decode nested request-body Encoding Objects", + SourceLocation( + encoding.provenance.node.resource, + encoding.provenance.node.pointer, + ), + ) + end + if startswith(base, "multipart/") && !isempty(encoding.headers) + _error!( + context.bag, + :unsupported_multipart_server_headers, + "server generation cannot recover custom multipart part headers", + SourceLocation( + encoding.provenance.node.resource, + encoding.provenance.node.pointer, + ), + ) + end + if base in ("application/x-www-form-urlencoded", "multipart/form-data") && + ( + encoding.style !== nothing || + encoding.explode !== nothing || + haskey(encoding.raw, "allowReserved") + ) + _error!( + context.bag, + :unsupported_server_encoding_style, + "server generation cannot invert explicit request-body encoding style, explode, or allowReserved fields", + SourceLocation( + encoding.provenance.node.resource, + encoding.provenance.node.pointer, + ), + ) + end + end end end return diff --git a/src/servergen.jl b/src/servergen.jl index 6dbeeaf..e2f3734 100644 --- a/src/servergen.jl +++ b/src/servergen.jl @@ -23,8 +23,11 @@ function _percent_decode(text::AbstractString; plus_space::Bool = false) index = 1 while index <= length(bytes) byte = bytes[index] - if byte == UInt8('%') && index + 2 <= length(bytes) && - _is_hex_byte(bytes[index + 1]) && _is_hex_byte(bytes[index + 2]) + if byte == UInt8('%') + index + 2 <= length(bytes) && + _is_hex_byte(bytes[index + 1]) && + _is_hex_byte(bytes[index + 2]) || + throw(_RequestFailure(400, "invalid percent-encoding in request")) write(io, parse(UInt8, String(Char.(bytes[index + 1:index + 2])); base = 16)) index += 3 elseif plus_space && byte == UInt8('+') @@ -265,13 +268,11 @@ function _decode_query_parameter(descriptor, pairs, consumed) value() = _percent_decode(String(pair.second); plus_space = true) if key == list_key push!(items, _header_atom(value())) - elseif startswith(key, prefix) && endswith(key, ']') && - ncodeunits(key) > ncodeunits(prefix) + 1 - inner = String(SubString( - key, - ncodeunits(prefix) + 1, - ncodeunits(key) - 1, - )) + elseif startswith(key, prefix) && endswith(key, ']') + start = nextind(key, lastindex(prefix)) + stop = prevind(key, lastindex(key)) + start <= stop || continue + inner = String(SubString(key, start, stop)) object[inner] = _header_atom(value()) elseif key == name scalar = _header_atom(value()) @@ -367,6 +368,21 @@ function _field_shape(fields, name) return :scalar end +function _field_value_kind(fields, name) + for field in fields + field.name == name || continue + return field.shape === :array ? field.item_kind : field.kind + end + return :default +end + +function _form_field_value(fields, name, text, context) + kind = _field_value_kind(fields, name) + kind === :object && return _parse_json(text, context) + kind in (:string, :bytes) && return String(text) + return _header_atom(text) +end + function _push_form_value!(object, fields, name, value) if haskey(object, name) existing = object[name] @@ -403,7 +419,12 @@ function _form_body_object(body, encodings, fields) for item in split(pair.second, delimiter) ] else - _header_atom(_percent_decode(String(pair.second); plus_space = true)) + _form_field_value( + fields, + name, + _percent_decode(String(pair.second); plus_space = true), + "decoding form field " * name, + ) end _push_form_value!(object, fields, name, value) end @@ -420,12 +441,19 @@ function _multipart_body_object(parts, encodings, fields) !isempty(declared) ? declared : encoding === nothing ? "" : something(encoding.content_type, ""), ) - # Text parts stay strings; binary parts become base64 so schema - # validation sees JSON-like values and base64-typed fields round trip. + kind = _field_value_kind(fields, name) value = if _is_json_media(media) _parse_json(part.data, "decoding multipart field " * name) - elseif isvalid(String, part.data) + elseif kind === :object + _parse_json(part.data, "decoding multipart field " * name) + elseif kind === :bytes + Base64.base64encode(part.data) + elseif kind === :string + isvalid(String, part.data) || + throw(_RequestFailure(400, "multipart string field is not valid UTF-8")) String(copy(part.data)) + elseif isvalid(String, part.data) + _header_atom(String(copy(part.data))) else Base64.base64encode(part.data) end @@ -608,16 +636,31 @@ function _success_response(responses) end function _server_response(operation, result) - result === nothing && return (204, Pair{String,String}[], UInt8[]) descriptor = _success_response(operation.responses) - if descriptor === nothing || isempty(descriptor.media) - throw(ArgumentError(string( + descriptor === nothing && throw(ArgumentError(string( + "operation ", + operation.id, + " documents no success response", + ))) + status = _selector_status(descriptor.selector) + if isempty(descriptor.media) + result === nothing || throw(ArgumentError(string( "operation ", operation.id, " documents no success response content; return `nothing` or a framework response", ))) + return (status, Pair{String,String}[], UInt8[]) + end + if result === nothing && all( + entry -> !_is_json_media(_base_media_type(entry[1])), + descriptor.media, + ) + throw(ArgumentError(string( + "operation ", + operation.id, + " cannot encode `nothing` using its documented non-JSON success media types", + ))) end - status = _selector_status(descriptor.selector) index = something( findfirst(entry -> _is_json_media(_base_media_type(entry[1])), descriptor.media), 1, @@ -766,7 +809,7 @@ function server_module_source( "# Generated by OpenAPI.jl from ", repr(plan.api.title), " version ", - plan.api.api_version, + repr(plan.api.api_version), ". Do not edit.", ) println(io, "# Implement these handler functions in a module (or any value") diff --git a/test/discriminators.jl b/test/discriminators.jl index 7502070..6d02c7e 100644 --- a/test/discriminators.jl +++ b/test/discriminators.jl @@ -206,6 +206,101 @@ @test pet.value.foo_bar_2 == 7 end + @testset "document text and generated locals cannot alter source" begin + field_names = [ + "raw", + "value", + "unknown", + "key", + "item", + "output", + "haskey", + "keys", + "collect", + "setdiff", + "isempty", + "join", + ] + summary = "literal \$(expression) and triple quote \"\"\"\nconst INJECTED_DOC = true" + document = OpenAPI.obj( + "openapi" => "3.1.1", + "info" => OpenAPI.obj( + "title" => "Quoted metadata", + "version" => "1.0\nconst INJECTED_VERSION = true", + ), + "paths" => OpenAPI.obj( + "/hostile" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "length", + "summary" => summary, + "parameters" => Any[ + OpenAPI.obj( + "name" => "values", + "in" => "query", + "schema" => OpenAPI.obj("type" => "string"), + ), + OpenAPI.obj( + "name" => "stream_to", + "in" => "query", + "schema" => OpenAPI.obj("type" => "string"), + ), + ], + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "hostile fields", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "\$ref" => "#/components/schemas/Hostile", + ), + ), + ), + ), + ), + ), + ), + ), + "components" => OpenAPI.obj( + "schemas" => OpenAPI.obj( + "Hostile" => OpenAPI.obj( + "type" => "object", + "required" => field_names, + "properties" => OpenAPI.obj( + (name => OpenAPI.obj("type" => "string") for name in field_names)..., + ), + "additionalProperties" => false, + ), + ), + ), + ) + + plan = OpenAPI.plan(document; name = "HostileClient") + operation = only(plan.operations) + @test operation.name == "length_" + @test [parameter.name for parameter in operation.parameters] == + ["values", "stream_to_2"] + source = OpenAPI.client(plan) + @test occursin("version \"1.0\\nconst INJECTED_VERSION = true\"", source) + host = Module(:HostileClientHost) + Base.include_string(host, source, "HostileClient.jl") + @test !isdefined(host, :INJECTED_VERSION) + client_module = Base.invokelatest(getfield, host, :HostileClient) + @test !isdefined(client_module, :INJECTED_DOC) + binding = Base.Docs.Binding(client_module, :length_) + doc = Base.Docs.docstr(binding, Tuple{}) + @test occursin(summary, String(only(doc.text))) + + raw = Dict(name => name for name in field_names) + value = Base.invokelatest(client_module._decode, client_module.Hostile, raw) + @test all(name -> getproperty(value, Symbol(name)) == name, field_names) + @test Base.invokelatest(client_module._encode, value) == raw + + server_source = OpenAPI.server(document; name = "HostileServer") + server_host = Module(:HostileServerHost) + Base.include_string(server_host, server_source, "HostileServer.jl") + @test !isdefined(server_host, :INJECTED_VERSION) + end + @testset "forward recursive union declarations" begin document = minimal_openapi( "3.1.1", diff --git a/test/runtests.jl b/test/runtests.jl index 3e404cd..80209f9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -28,6 +28,26 @@ getschema(reg, T) = OpenAPI.schemaof(reg, T) @testset "OpenAPI" begin + @testset "public API uses the package namespace" begin + for name in ( + :Operation, + :Param, + :check, + :client, + :document, + :load, + :normalize, + :plan, + :server, + :server_source, + :serverplan, + :validate, + ) + @test Base.ispublic(OpenAPI, name) + @test !Base.isexported(OpenAPI, name) + end + end + @testset "Julia types -> JSON Schema" begin reg = OpenAPI.SchemaRegistry() @test getschema(reg, Int)["type"] == "integer" diff --git a/test/runtime.jl b/test/runtime.jl index 26663a1..7f58e19 100644 --- a/test/runtime.jl +++ b/test/runtime.jl @@ -251,6 +251,40 @@ @test invoke(:_media_match_score, "text/plain", "text/*") == 2 @test invoke(:_media_match_score, "application/json", "*/*") == 1 @test invoke(:_media_match_score, "application/json", "text/*") == 0 + @test invoke( + :_media_match, + "application/json; stream=watch", + "application/json;stream=watch", + ) + @test !invoke( + :_media_match, + "application/json", + "application/json;stream=watch", + ) + @test invoke( + :_media_match, + "application/json; charset=UTF-8", + "application/json;charset=utf-8", + ) + + plain = ("application/json", :plain) + watch = ("application/json;stream=watch", :watch) + @test invoke( + :_select_media, + (plain, watch), + "application/json; stream=watch", + ) == watch + @test invoke( + :_select_media, + (watch, plain), + "application/json", + ) == plain + quoted = ("application/json;profile=\"a;b\"", :quoted) + @test invoke( + :_select_media, + (plain, quoted), + "application/json; profile=\"a;b\"", + ) == quoted responses = ( (selector = "default", media = (), headers = ()), diff --git a/test/runtime_integration.jl b/test/runtime_integration.jl index 9582894..09c2573 100644 --- a/test/runtime_integration.jl +++ b/test/runtime_integration.jl @@ -978,6 +978,7 @@ end write(socket, "0\r\n\r\n") flush(socket) end + forever_stopped = Channel{Nothing}(1) accept_task = @async while isopen(raw_server) socket = try Sockets.accept(raw_server) @@ -1032,11 +1033,13 @@ end ) flush(socket) elseif startswith(path, "/stream/forever") - write(socket, chunked_head("application/json")) - flush(socket) - for tick in 1:1000 - send_chunk(socket, """{"kind":"TICK","seq":$tick}\n""") - sleep(0.02) + try + write(socket, chunked_head("application/json")) + flush(socket) + send_chunk(socket, """{"kind":"TICK","seq":1}\n""") + read(socket, 1) + finally + put!(forever_stopped, nothing) end end close(socket) @@ -1097,6 +1100,8 @@ end first_tick = take!(forever_channel) @test first_tick.kind == "TICK" close(forever_channel) + @test Base.timedwait(() -> isready(forever_stopped), 5) == :ok + take!(forever_stopped) finally close(raw_server) end diff --git a/test/servergen.jl b/test/servergen.jl index 39e79e9..edd10c7 100644 --- a/test/servergen.jl +++ b/test/servergen.jl @@ -127,7 +127,7 @@ const SERVER_ROUNDTRIP_DOCUMENT = OpenAPI.obj( "application/x-www-form-urlencoded" => OpenAPI.obj( "schema" => OpenAPI.obj( "type" => "object", - "required" => Any["q"], + "required" => Any["q", "meta"], "properties" => OpenAPI.obj( "q" => OpenAPI.obj("type" => "string"), "limit" => OpenAPI.obj("type" => "integer"), @@ -135,6 +135,14 @@ const SERVER_ROUNDTRIP_DOCUMENT = OpenAPI.obj( "type" => "array", "items" => OpenAPI.obj("type" => "string"), ), + "meta" => OpenAPI.obj( + "type" => "object", + "required" => Any["code"], + "properties" => OpenAPI.obj( + "code" => OpenAPI.obj("type" => "string"), + ), + "additionalProperties" => false, + ), ), ), ), @@ -154,6 +162,15 @@ const SERVER_ROUNDTRIP_DOCUMENT = OpenAPI.obj( "type" => "array", "items" => OpenAPI.obj("type" => "string"), ), + "meta" => OpenAPI.obj( + "type" => "object", + "required" => Any["code"], + "properties" => OpenAPI.obj( + "code" => OpenAPI.obj("type" => "string"), + ), + "additionalProperties" => false, + ), + "raw" => OpenAPI.obj("type" => "string"), ), ), ), @@ -171,10 +188,14 @@ const SERVER_ROUNDTRIP_DOCUMENT = OpenAPI.obj( "multipart/form-data" => OpenAPI.obj( "schema" => OpenAPI.obj( "type" => "object", - "required" => Any["note"], + "required" => Any["note", "file", "count"], "properties" => OpenAPI.obj( "note" => OpenAPI.obj("type" => "string"), - "file" => OpenAPI.obj("type" => "string"), + "file" => OpenAPI.obj( + "type" => "string", + "format" => "binary", + ), + "count" => OpenAPI.obj("type" => "integer"), ), ), ), @@ -190,6 +211,7 @@ const SERVER_ROUNDTRIP_DOCUMENT = OpenAPI.obj( "properties" => OpenAPI.obj( "note" => OpenAPI.obj("type" => "string"), "bytes" => OpenAPI.obj("type" => "integer"), + "count" => OpenAPI.obj("type" => "integer"), ), ), ), @@ -251,6 +273,31 @@ const SERVER_ROUNDTRIP_DOCUMENT = OpenAPI.obj( ), ), ), + "/empty-200" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "empty200", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj("description" => "empty success"), + ), + ), + ), + "/null" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "nullOut", + "responses" => OpenAPI.obj( + "200" => OpenAPI.obj( + "description" => "nullable success", + "content" => OpenAPI.obj( + "application/json" => OpenAPI.obj( + "schema" => OpenAPI.obj( + "type" => Any["string", "null"], + ), + ), + ), + ), + ), + ), + ), ), "components" => OpenAPI.obj( "schemas" => OpenAPI.obj( @@ -328,11 +375,14 @@ submitform(req, body) = (; q = body.q, limit = body.limit isa Int ? body.limit : 0, tags = body.tags isa Vector ? body.tags : String[], + meta = body.meta, + raw = String(copy(req.body)), ) upload(req, body) = (; note = body.note, - bytes = body.file isa AbstractString ? ncodeunits(body.file) : 0, + bytes = length(body.file), + count = body.count, ) textout(req) = "plain text" @@ -342,6 +392,10 @@ binout(req) = Vector{UInt8}(codeunits("raw-bytes")) custom(req) = HTTP.Response(418, ["Content-Type" => "text/plain"], "teapot") nocontent(req) = nothing + +empty200(req) = nothing + +nullout(req) = nothing """ @testset "server generation" begin @@ -440,33 +494,45 @@ nocontent(req) = nothing end @testset "form and multipart bodies round trip" begin + meta = call(:SubmitformRequestMetaModel; code = "001") form_body = call( - :SubmitformRequest, - "search term", - 5, - ["solo"], - Dict{String,Any}(), + :SubmitformRequest; + q = "001", + limit = 5, + tags = ["solo"], + meta, ) form_echo = call(:submitform, form_body) - @test form_echo.q == "search term" + @test form_echo.q == "001" @test form_echo.limit == 5 @test form_echo.tags == ["solo"] + @test form_echo.meta.code == "001" + @test !isempty(form_echo.raw) + @test occursin("q=001", form_echo.raw) + bytes = Vector{UInt8}(codeunits("valid UTF-8 binary")) upload_body = call( - :UploadModelRequest, - "note text", - "file contents here", - Dict{String,Any}(), + :UploadModelRequest; + note = "note text", + file = bytes, + count = 7, ) upload_echo = call(:upload, upload_body) @test upload_echo.note == "note text" - @test upload_echo.bytes == ncodeunits("file contents here") + @test upload_echo.bytes == length(bytes) + @test upload_echo.count == 7 end @testset "response encodings" begin @test call(:textout) == "plain text" @test call(:binout) == "raw-bytes" @test call(:nocontent) === nothing + empty = call(:empty200; with_http_info = true) + @test empty.status == 200 + @test empty.body === nothing + nullable = call(:nullout; with_http_info = true) + @test nullable.status == 200 + @test nullable.body === nothing end @testset "framework response passthrough" begin @@ -476,6 +542,24 @@ nocontent(req) = nothing end @testset "request error responses" begin + RequestFailure = Base.invokelatest(getfield, S, :_RequestFailure) + @test_throws RequestFailure Base.invokelatest( + getfield(S, :_percent_decode), + "%ZZ", + ) + @test_throws RequestFailure Base.invokelatest( + getfield(S, :_percent_decode), + "%", + ) + + unicode = HTTP.get( + "$base/styles/a/.l/;matrix=1/;mexp=1?qform=x&deep[%C3%A9]=value"; + status_exception = false, + ) + @test unicode.status == 200 + unicode_echo = JSON.parse(unicode.body) + @test unicode_echo["deep"]["é"] == "value" + bad_path = HTTP.get( "$base/styles/a/.l/;matrix=oops/;mexp=1?qform=x"; status_exception = false, @@ -572,6 +656,96 @@ end @test OpenAPI.plan(document) isa OpenAPI.ClientPlan end + @testset "lossy request-body encodings are rejected" begin + function body_encoding_document(schema, encoding) + return OpenAPI.obj( + "openapi" => "3.2.0", + "info" => OpenAPI.obj("title" => "Encoding", "version" => "1.0.0"), + "paths" => OpenAPI.obj( + "/encoded" => OpenAPI.obj( + "post" => OpenAPI.obj( + "operationId" => "encoded", + "requestBody" => OpenAPI.obj( + "content" => OpenAPI.obj( + "multipart/form-data" => OpenAPI.obj( + "schema" => schema, + "encoding" => encoding, + ), + ), + ), + "responses" => OpenAPI.obj( + "204" => OpenAPI.obj("description" => "empty"), + ), + ), + ), + ), + ) + end + + nested = body_encoding_document( + OpenAPI.obj( + "type" => "object", + "properties" => OpenAPI.obj( + "bundle" => OpenAPI.obj( + "type" => "object", + "properties" => OpenAPI.obj( + "child" => OpenAPI.obj("type" => "string"), + ), + ), + ), + ), + OpenAPI.obj( + "bundle" => OpenAPI.obj( + "contentType" => "multipart/form-data", + "encoding" => OpenAPI.obj( + "child" => OpenAPI.obj("contentType" => "text/plain"), + ), + ), + ), + ) + headers = body_encoding_document( + OpenAPI.obj( + "type" => "object", + "properties" => OpenAPI.obj( + "file" => OpenAPI.obj("type" => "string", "format" => "binary"), + ), + ), + OpenAPI.obj( + "file" => OpenAPI.obj( + "headers" => OpenAPI.obj( + "X-Part" => OpenAPI.obj( + "schema" => OpenAPI.obj("type" => "string"), + ), + ), + ), + ), + ) + styled = body_encoding_document( + OpenAPI.obj( + "type" => "object", + "properties" => OpenAPI.obj( + "items" => OpenAPI.obj( + "type" => "array", + "items" => OpenAPI.obj("type" => "string"), + ), + ), + ), + OpenAPI.obj( + "items" => OpenAPI.obj("style" => "form", "explode" => false), + ), + ) + + for (document, code) in ( + (nested, :unsupported_nested_server_encoding), + (headers, :unsupported_multipart_server_headers), + (styled, :unsupported_server_encoding_style), + ) + error = @test_throws OpenAPI.OpenAPIError OpenAPI.serverplan(document) + @test code in Set(diagnostic.code for diagnostic in error.value.diagnostics) + @test OpenAPI.plan(document) isa OpenAPI.ClientPlan + end + end + @testset "ambiguous exploded object parameters are rejected" begin exploded(name) = OpenAPI.obj( "name" => name, From fd4558c01bac8d3988cd8e2d76a168de24569e08 Mon Sep 17 00:00:00 2001 From: Jacob Quinn Date: Mon, 10 Aug 2026 02:42:21 -0600 Subject: [PATCH 7/9] fix(client): honor tolerant response decoding Thread validate_responses through nested generated model decoders. Preserve explicit null on optional fields when validation is disabled. Apply parameter-aware custom decoders to each framed stream item so callers can override inaccurate watch response schemas. --- README.md | 23 +++- src/client.jl | 227 +++++++++++++++++++++++++++--------- src/planning.jl | 9 +- test/runtime_integration.jl | 120 ++++++++++++++++++- 4 files changed, 318 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 6b34169..cb93eb4 100644 --- a/README.md +++ b/README.md @@ -255,7 +255,12 @@ Set `validate_requests=false` or `validate_responses=false` on a generated `Client` only when the application accepts that loss of boundary validation. For example, a response schema with `additionalProperties: false` rejects a new server field. This is contract-correct but can make a client less tolerant of -an API that changes outside its published contract. +an API that changes outside its published contract. With response validation +disabled, that policy also reaches nested generated models. Unknown response +properties are ignored, and an explicit null on an optional response property +decodes to `nothing` even when the document marks that property non-nullable. +Missing optional properties still decode to `ABSENT`. Values that cannot fit +the generated Julia type can still raise `DecodeError`. ## HTTP behavior @@ -319,6 +324,22 @@ response ends, closes with the error when decoding or validation fails, and closing it from the consumer side aborts the transfer. Error statuses still throw `ApiError` with the fully buffered error body. +A registered response decoder also applies to streaming calls. The runtime +calls it once for each framed item and puts its return value directly on the +channel. This is an escape hatch for deployed APIs whose streaming wire format +does not match the response schema. Register the full parameterized media type +to limit the override to that stream: + +```julia +ExampleClient.codec!( + client, + "application/json;stream=watch"; + decode = (bytes, media_type) -> JSON.parse(String(bytes)), +) +``` + +This decoder does not replace the decoder for plain `application/json`. + For a custom media type, register an encoder or decoder: ```julia diff --git a/src/client.jl b/src/client.jl index 1876945..358eff9 100644 --- a/src/client.jl +++ b/src/client.jl @@ -254,23 +254,32 @@ function _decode(::Type{Vector{UInt8}}, value::AbstractString) end _decode(::Type{Vector{UInt8}}, value::AbstractVector{UInt8}) = copy(value) -function _decode(::Type{T}, value::AbstractVector) where {T<:AbstractVector} +_decode(::Type{T}, value::AbstractVector) where {T<:AbstractVector} = + _decode(T, value, true) +function _decode(::Type{T}, value::AbstractVector, validate::Bool) where {T<:AbstractVector} element = eltype(T) - return T([_decode(element, item) for item in value]) + return T([_decode(element, item, validate) for item in value]) end -function _decode(::Type{T}, value::AbstractDict) where {T<:AbstractDict} +_decode(::Type{T}, value::AbstractDict) where {T<:AbstractDict} = + _decode(T, value, true) +function _decode(::Type{T}, value::AbstractDict, validate::Bool) where {T<:AbstractDict} keytype(T) <: AbstractString || throw(DecodeError("only string-key dictionaries are supported, got $T")) output = T() for (key, item) in value - output[convert(keytype(T), key)] = _decode(valtype(T), item) + output[convert(keytype(T), key)] = _decode(valtype(T), item, validate) end return output end -function _decode(::Type{T}, value::AbstractVector) where {T<:Tuple} +_decode(::Type{T}, value::AbstractVector) where {T<:Tuple} = + _decode(T, value, true) +function _decode(::Type{T}, value::AbstractVector, validate::Bool) where {T<:Tuple} length(value) == fieldcount(T) || throw(DecodeError("expected $(fieldcount(T)) tuple items, got $(length(value))")) - return T((_decode(fieldtype(T, index), value[index]) for index in 1:fieldcount(T))...) + return T(( + _decode(fieldtype(T, index), value[index], validate) for + index in 1:fieldcount(T) + )...) end function _direct_union_match(::Type{T}, value) where {T} @@ -284,7 +293,12 @@ function _direct_union_match(::Type{T}, value) where {T} return value isa T end -function _decode_union(::Type{T}, value; oneof::Bool = false) where {T} +function _decode_union( + ::Type{T}, + value; + oneof::Bool = false, + validate::Bool = true, +) where {T} variants = Base.uniontypes(T) if value === nothing && Nothing in variants return nothing @@ -302,7 +316,7 @@ function _decode_union(::Type{T}, value; oneof::Bool = false) where {T} for variant in variants variant in (Absent, Nothing) && continue try - decoded = _decode(variant, value) + decoded = _decode(variant, value, validate) oneof || return decoded push!(successes, decoded) catch error @@ -327,6 +341,11 @@ function _decode(::Type{T}, value) where {T} end end +function _decode(::Type{T}, value, validate::Bool) where {T} + T isa Union && return _decode_union(T, value; validate) + return _decode(T, value) +end + _encode(::Absent) = throw(ArgumentError("ABSENT is only valid as an object field")) _encode(::Nothing) = nothing _encode(value::Union{Bool,Number,AbstractString}) = value @@ -477,6 +496,29 @@ function _select_media(media, received) return selected end +function _select_media_codec(codecs, received) + selected = nothing + score = (0, 0, 0) + for (documented, codec) in codecs + candidate = _media_selection_score(String(received), documented) + if candidate > score + selected = codec + score = candidate + end + end + return selected +end + +function _store_media_codec!(codecs, media_type, codec) + key = strip(String(media_type)) + parsed = _media_type(key) + for existing in collect(keys(codecs)) + _media_type(existing) == parsed && delete!(codecs, existing) + end + codecs[key] = codec + return codecs +end + function _select_response(responses, status) exact = string(status) range = string(div(status, 100), "XX") @@ -550,6 +592,7 @@ function _decode_schema_header( set_cookie::Bool = false, direction::Symbol = :output, context = "decoding a response header", + validate::Bool = true, ) selected = _header_type_variant(type, shape) raw = if shape === :array @@ -595,13 +638,13 @@ function _decode_schema_header( else _header_scalar(selected, join(values, set_cookie ? '\n' : ',')) end - _validate_schema( + validate && _validate_schema( schema, _encode(raw), context; direction, ) - return _decode(type, raw) + return _decode(type, raw, validate) end _is_json_media(media) = media == "application/json" || endswith(media, "+json") @@ -811,20 +854,14 @@ mutable struct Client validate_responses::Bool end -function _normalize_media_codecs(codecs::Dict{String,Function}) +function _normalize_media_codecs(codecs::AbstractDict) normalized = Dict{String,Function}() - for name in keys(codecs) - normalized[_base_media_type(name)] = codecs[name] + for (name, codec) in codecs + _store_media_codec!(normalized, name, codec) end return normalized end -function _normalize_media_codecs(codecs::AbstractDict) - return Dict{String,Function}( - _base_media_type(name) => codec for (name, codec) in codecs - ) -end - function Client( server::Union{Nothing,AbstractString} = nothing; server_index::Integer = 1, @@ -916,9 +953,10 @@ function codec!( encode::Union{Nothing,Function} = nothing, decode::Union{Nothing,Function} = nothing, ) - key = _base_media_type(media_type) - encode === nothing || (client.media_encoders[key] = encode) - decode === nothing || (client.media_decoders[key] = decode) + encode === nothing || + _store_media_codec!(client.media_encoders, media_type, encode) + decode === nothing || + _store_media_codec!(client.media_decoders, media_type, decode) return client end codec!(media_type::AbstractString; kwargs...) = @@ -1347,8 +1385,9 @@ function _decode_response_headers(client, descriptor, headers) values, header.shape, header.explode, - client.validate_responses ? header.schema : nothing, + header.schema, set_cookie = lowercase(header.name) == "set-cookie", + validate = client.validate_responses, ) else entry = first(header.content) @@ -1368,7 +1407,7 @@ end function _decode_body(client::Client, type, content_type, body, schema) media = _base_media_type(content_type) - decoder = get(client.media_decoders, media, nothing) + decoder = _select_media_codec(client.media_decoders, content_type) if decoder !== nothing value = decoder(copy(body), String(content_type)) lowered = _encode(value) @@ -1379,7 +1418,7 @@ function _decode_body(client::Client, type, content_type, body, schema) "decoding a custom-media response"; direction = :output, ) - return _decode(type, lowered) + return _decode(type, lowered, client.validate_responses) elseif _is_json_media(media) if isempty(body) client.validate_responses && @@ -1389,7 +1428,7 @@ function _decode_body(client::Client, type, content_type, body, schema) "decoding an empty JSON response"; direction = :output, ) - return _decode(type, nothing) + return _decode(type, nothing, client.validate_responses) end value = _parse_json(body, "decoding a response") client.validate_responses && @@ -1399,7 +1438,7 @@ function _decode_body(client::Client, type, content_type, body, schema) "decoding a JSON response"; direction = :output, ) - return _decode(type, value) + return _decode(type, value, client.validate_responses) elseif _is_sequential_json_media(media) value = _decode_sequential_json(body, media) client.validate_responses && @@ -1409,7 +1448,7 @@ function _decode_body(client::Client, type, content_type, body, schema) "decoding a sequential JSON response"; direction = :output, ) - return _decode(type, value) + return _decode(type, value, client.validate_responses) elseif startswith(media, "text/") || type === String isvalid(String, body) || throw(DecodeError("response text is not UTF-8")) value = String(copy(body)) @@ -1420,7 +1459,7 @@ function _decode_body(client::Client, type, content_type, body, schema) "decoding a text response"; direction = :output, ) - return _decode(type, value) + return _decode(type, value, client.validate_responses) elseif type === Vector{UInt8} || type === Any || media == "application/octet-stream" client.validate_responses && _validate_schema( schema, @@ -1428,7 +1467,8 @@ function _decode_body(client::Client, type, content_type, body, schema) "decoding a binary response", ; direction = :output, ) - return type === Any ? copy(body) : _decode(type, body) + return type === Any ? copy(body) : + _decode(type, body, client.validate_responses) end throw(UnsupportedMediaType(String(content_type), :response)) end @@ -1464,7 +1504,7 @@ function _encoded_media_value(client, value, media_type) elseif startswith(media, "text/") return _scalar(value) end - encoder = get(client.media_encoders, media, nothing) + encoder = _select_media_codec(client.media_encoders, media_type) encoder === nothing || return encoder(value, String(media_type)) value isa Upload && return value.data value isa AbstractVector{UInt8} && return value @@ -2203,6 +2243,9 @@ end # newline-separated JSON documents, e.g. watch-style endpoints); sequential # JSON media types split records and decode each to the documented array's # element type; `text/*` yields lines; anything else yields raw byte chunks. +# A registered decoder receives each framed item and its actual Content-Type. +# Its return value is delivered directly, so it can override an inaccurate +# response schema such as a Kubernetes List declaration on a watch stream. function _stream_plan(media, type, schema) if _is_sequential_json_media(media) item_type = type <: AbstractVector && type !== Vector{UInt8} ? @@ -2310,12 +2353,22 @@ function _next_stream_frame!(buffer::Vector{UInt8}, kind::Symbol, final::Bool) return _next_stream_line!(buffer, final) end -function _decode_stream_item(client::Client, frame::Vector{UInt8}, kind::Symbol, item_type, schema) +function _decode_stream_item( + client::Client, + frame::Vector{UInt8}, + kind::Symbol, + item_type, + schema, + media_type, +) + decoder = _select_media_codec(client.media_decoders, media_type) + decoder === nothing || return decoder(copy(frame), String(media_type)) if kind === :text isvalid(String, frame) || throw(DecodeError("streaming text response is not UTF-8")) - return _decode(item_type, String(frame)) + return _decode(item_type, String(frame), client.validate_responses) end + kind === :bytes && return copy(frame) value = _parse_json(frame, "decoding a streaming response item") schema === nothing || !client.validate_responses || _validate_schema( schema, @@ -2323,7 +2376,7 @@ function _decode_stream_item(client::Client, frame::Vector{UInt8}, kind::Symbol, "decoding a streaming response item"; direction = :output, ) - return _decode(item_type, value) + return _decode(item_type, value, client.validate_responses) end function _drain_stream_buffer!( @@ -2333,12 +2386,16 @@ function _drain_stream_buffer!( kind::Symbol, item_type, schema, + media_type, final::Bool, ) while true frame = _next_stream_frame!(buffer, kind, final) frame === nothing && break - put!(channel, _decode_stream_item(client, frame, kind, item_type, schema)) + put!( + channel, + _decode_stream_item(client, frame, kind, item_type, schema, media_type), + ) end final && kind === :json && !all(_stream_whitespace, buffer) && throw(DecodeError("streaming response ended with a truncated item")) @@ -2352,6 +2409,7 @@ function _pump_stream!( kind::Symbol, item_type, schema, + media_type, finished, ) buffer = UInt8[] @@ -2360,7 +2418,17 @@ function _pump_stream!( chunk = readavailable(stream) isempty(chunk) && continue if kind === :bytes - put!(channel, chunk) + put!( + channel, + _decode_stream_item( + client, + chunk, + kind, + item_type, + schema, + media_type, + ), + ) else append!(buffer, chunk) _drain_stream_buffer!( @@ -2370,6 +2438,7 @@ function _pump_stream!( kind, item_type, schema, + media_type, false, ) end @@ -2381,6 +2450,7 @@ function _pump_stream!( kind, item_type, schema, + media_type, true, ) finished[] = true @@ -2470,11 +2540,12 @@ function _stream_request( with_http_info, ) end - local descriptor, decoded_headers, kind, item_type, schema + local descriptor, decoded_headers, kind, item_type, schema, actual_media try descriptor = _select_response(operation.responses, status) decoded_headers = _decode_response_headers(client, descriptor, response_headers) if descriptor === nothing || isempty(descriptor.media) + actual_media = isempty(received) ? "application/octet-stream" : String(received) kind, item_type, schema = :bytes, Vector{UInt8}, nothing else selected, actual_media = @@ -2497,6 +2568,7 @@ function _stream_request( kind, item_type, schema, + actual_media, finished, )) errormonitor(@async _abort_stream_on_close!(stream, stream_to, producer, finished)) @@ -2723,9 +2795,18 @@ function _emit_model( model.name, ", value)", ) - println(io, "function _decode(::Type{", model.name, "}, value)") - println(io, " _validate_schema(", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") - println(io, " return ", model.name, "(_decode(", type, ", value))") + model.name in abstract_targets && println( + io, + "_decode(::Type{Abstract", + model.name, + "}, value, validate::Bool) = _decode(", + model.name, + ", value, validate)", + ) + println(io, "_decode(::Type{", model.name, "}, value) = _decode(", model.name, ", value, true)") + println(io, "function _decode(::Type{", model.name, "}, value, _openapi_validate::Bool)") + println(io, " _openapi_validate && _validate_schema(", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") + println(io, " return ", model.name, "(_decode(", type, ", value, _openapi_validate))") println(io, "end") println(io, "function _encode(value::", model.name, ")") println(io, " output = _encode(value.value)") @@ -2752,9 +2833,18 @@ function _emit_model( model.name, ", value)", ) - println(io, "function _decode(::Type{", model.name, "}, value)") - println(io, " _validate_schema(", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") - println(io, " return ", model.name, "(_decode(", model.alias, ", value))") + model.name in abstract_targets && println( + io, + "_decode(::Type{Abstract", + model.name, + "}, value, validate::Bool) = _decode(", + model.name, + ", value, validate)", + ) + println(io, "_decode(::Type{", model.name, "}, value) = _decode(", model.name, ", value, true)") + println(io, "function _decode(::Type{", model.name, "}, value, _openapi_validate::Bool)") + println(io, " _openapi_validate && _validate_schema(", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") + println(io, " return ", model.name, "(_decode(", model.alias, ", value, _openapi_validate))") println(io, "end") println(io, "function _encode(value::", model.name, ")") println(io, " output = _encode(value.value)") @@ -2776,10 +2866,19 @@ function _emit_model( model.name, ", value)", ) + model.name in abstract_targets && println( + io, + "_decode(::Type{Abstract", + model.name, + "}, value, validate::Bool) = _decode(", + model.name, + ", value, validate)", + ) if model.discriminator !== nothing && (!isempty(model.discriminator_mapping) || model.discriminator_default !== nothing) - println(io, "function _decode(::Type{", model.name, "}, value)") - println(io, " _validate_schema(", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") + println(io, "_decode(::Type{", model.name, "}, value) = _decode(", model.name, ", value, true)") + println(io, "function _decode(::Type{", model.name, "}, value, _openapi_validate::Bool)") + println(io, " _openapi_validate && _validate_schema(", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") println(io, " object = _object(value, ", repr(model.name), ")") println(io, " tag = get(object, ", repr(model.discriminator), ", ABSENT)") println(io, " tag isa Absent || tag isa AbstractString || throw(DecodeError(\"discriminator value must be a string for ", model.name, "\"))") @@ -2810,12 +2909,13 @@ function _emit_model( ) end println(io, " selected === nothing && throw(DecodeError(\"unknown discriminator value \$(repr(tag)) for ", model.name, "\"))") - println(io, " _schema_valid(selected[2], value; direction = ", direction, ") || throw(DecodeError(\"discriminator-selected schema did not validate for ", model.name, "\"))") - println(io, " return ", model.name, "(_decode(selected[1], value))") + println(io, " !_openapi_validate || _schema_valid(selected[2], value; direction = ", direction, ") || throw(DecodeError(\"discriminator-selected schema did not validate for ", model.name, "\"))") + println(io, " return ", model.name, "(_decode(selected[1], value, _openapi_validate))") println(io, "end") else - println(io, "function _decode(::Type{", model.name, "}, value)") - println(io, " _validate_schema(", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") + println(io, "_decode(::Type{", model.name, "}, value) = _decode(", model.name, ", value, true)") + println(io, "function _decode(::Type{", model.name, "}, value, _openapi_validate::Bool)") + println(io, " _openapi_validate && _validate_schema(", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") "Nothing" in model.values && println( io, " value === nothing && return ", @@ -2825,8 +2925,12 @@ function _emit_model( println(io, " matches = Any[]") for (node, type) in model.variants descriptor = _schema_descriptor(node) - println(io, " if _schema_valid(", descriptor, ", value; direction = ", direction, ")") - println(io, " push!(matches, _decode(", type, ", value))") + println(io, " if !_openapi_validate || _schema_valid(", descriptor, ", value; direction = ", direction, ")") + println(io, " try") + println(io, " push!(matches, _decode(", type, ", value, _openapi_validate))") + println(io, " catch error") + println(io, " error isa DecodeError || rethrow()") + println(io, " end") println(io, " end") end if model.kind === :oneof @@ -2877,8 +2981,17 @@ function _emit_model( model.name, ", value)", ) - println(io, "function _decode(::Type{", model.name, "}, _openapi_raw)") - println(io, " _validate_schema(", schema, ", _openapi_raw, ", repr("decoding " * model.name), "; direction = ", direction, ")") + model.name in abstract_targets && println( + io, + "_decode(::Type{Abstract", + model.name, + "}, value, validate::Bool) = _decode(", + model.name, + ", value, validate)", + ) + println(io, "_decode(::Type{", model.name, "}, value) = _decode(", model.name, ", value, true)") + println(io, "function _decode(::Type{", model.name, "}, _openapi_raw, _openapi_validate::Bool)") + println(io, " _openapi_validate && _validate_schema(", schema, ", _openapi_raw, ", repr("decoding " * model.name), "; direction = ", direction, ")") println(io, " _openapi_object = _object(_openapi_raw, ", repr(model.name), ")") for field in model.fields type = _rewrite_forward(field.type, index, indices, abstract_targets) @@ -2894,7 +3007,7 @@ function _emit_model( repr(field.wire_name), ", ", repr(model.name), - "))", + "), _openapi_validate)", ) else println( @@ -2907,7 +3020,7 @@ function _emit_model( type, ", _openapi_object[", repr(field.wire_name), - "]) : ABSENT", + "], _openapi_validate) : ABSENT", ) end end @@ -2917,11 +3030,11 @@ function _emit_model( println(io, " _openapi_additional_properties = Dict{String,", additional_type, "}()") println(io, " for (_openapi_key, _openapi_item) in _openapi_object") println(io, " String(_openapi_key) in ", _julia_literal(known), " && continue") - println(io, " _openapi_additional_properties[String(_openapi_key)] = _decode(", additional_type, ", _openapi_item)") + println(io, " _openapi_additional_properties[String(_openapi_key)] = _decode(", additional_type, ", _openapi_item, _openapi_validate)") println(io, " end") else println(io, " _openapi_unknown = setdiff(String.(collect(keys(_openapi_object))), collect(", _julia_literal(known), "))") - println(io, " isempty(_openapi_unknown) || throw(DecodeError(\"unknown fields while decoding ", model.name, ": \" * join(_openapi_unknown, \", \")))") + println(io, " !_openapi_validate || isempty(_openapi_unknown) || throw(DecodeError(\"unknown fields while decoding ", model.name, ": \" * join(_openapi_unknown, \", \")))") end print(io, " return ", model.name, "(") assignments = String[ diff --git a/src/planning.jl b/src/planning.jl index 2d4aeff..ee5d22d 100644 --- a/src/planning.jl +++ b/src/planning.jl @@ -846,13 +846,20 @@ function _plan_object!(context, view, suggested, mode) count > 1 && (field_name = string(field_name, '_', count)) base = _type_for!(context, child, name * _type_identifier(wire_name), mode) nullable = _nullable(context, child) + # Response validation can be disabled to tolerate deployed APIs that + # return explicit null for an optional, non-nullable property. Models + # without readOnly/writeOnly properties are shared by input and output + # planning, so every optional model field must be able to represent the + # response value. Keep `Absent` for missing and `Nothing` for present + # null; normal boundary validation still enforces the schema. + decodable_null = nullable || !required default = required ? nothing : "ABSENT" push!( fields, ModelFieldPlan( field_name, wire_name, - _field_type(base, required, nullable), + _field_type(base, required, decodable_null), required, nullable, default, diff --git a/test/runtime_integration.jl b/test/runtime_integration.jl index 09c2573..56e4629 100644 --- a/test/runtime_integration.jl +++ b/test/runtime_integration.jl @@ -115,6 +115,12 @@ end ["Content-Type" => "text/plain"], UInt8[0xff], ) + elseif startswith(path, "/drift") + return HTTP.Response( + 200, + ["Content-Type" => "application/json"], + """{"items":[{"probe":{"lastProbeTime":null,"newServerField":true}}]}""", + ) elseif startswith(path, "/sequence") return HTTP.Response( 200, @@ -218,6 +224,43 @@ end ), "additionalProperties" => false, ) + event_list_schema = OpenAPI.obj( + "type" => "object", + "required" => ["items"], + "properties" => OpenAPI.obj( + "items" => OpenAPI.obj("type" => "array", "items" => event_schema), + ), + "additionalProperties" => false, + ) + probe_schema = OpenAPI.obj( + "type" => "object", + "properties" => OpenAPI.obj( + "lastProbeTime" => OpenAPI.obj( + "type" => "string", + "format" => "date-time", + ), + ), + "additionalProperties" => false, + ) + pod_schema = OpenAPI.obj( + "type" => "object", + "required" => ["probe"], + "properties" => OpenAPI.obj( + "probe" => OpenAPI.obj("\$ref" => "#/components/schemas/Probe"), + ), + "additionalProperties" => false, + ) + pod_list_schema = OpenAPI.obj( + "type" => "object", + "required" => ["items"], + "properties" => OpenAPI.obj( + "items" => OpenAPI.obj( + "type" => "array", + "items" => OpenAPI.obj("\$ref" => "#/components/schemas/Pod"), + ), + ), + "additionalProperties" => false, + ) paths = OpenAPI.obj( "/styles/{simple}/{label}/{matrix}" => OpenAPI.obj( @@ -521,6 +564,17 @@ end ), ), ), + "/drift" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "driftList", + "responses" => OpenAPI.obj( + "200" => runtime_response( + "application/json", + OpenAPI.obj("\$ref" => "#/components/schemas/PodList"), + ), + ), + ), + ), "/sequence" => OpenAPI.obj( "get" => OpenAPI.obj( "operationId" => "sequence", @@ -543,6 +597,17 @@ end ), ), ), + "/stream/custom-watch" => OpenAPI.obj( + "get" => OpenAPI.obj( + "operationId" => "customWatchStream", + "responses" => OpenAPI.obj( + "200" => runtime_response( + "application/json; stream=watch", + event_list_schema, + ), + ), + ), + ), "/stream/logs" => OpenAPI.obj( "get" => OpenAPI.obj( "operationId" => "logStream", @@ -686,7 +751,12 @@ end ], "paths" => paths, "components" => OpenAPI.obj( - "schemas" => OpenAPI.obj("Payload" => payload_schema), + "schemas" => OpenAPI.obj( + "Payload" => payload_schema, + "Probe" => probe_schema, + "Pod" => pod_schema, + "PodList" => pod_list_schema, + ), "securitySchemes" => OpenAPI.obj( "HeaderKey" => OpenAPI.obj( "type" => "apiKey", @@ -945,6 +1015,13 @@ end @test_throws C.DecodeError call(:badtext; client) take_request() + @test_throws C.SchemaValidationError call(:driftlist; client) + take_request() + tolerant_client = C.Client(; validate_responses = false) + drifted = call(:driftlist; client = tolerant_client) + take_request() + @test drifted.items[1].probe.lastprobetime === nothing + @test call(:undocumentedstatus, 204; client) === nothing take_request() undocumented_bytes = call(:undocumentedstatus, 201; client) @@ -990,7 +1067,13 @@ end while !isempty(readline(socket)) end path = split(request_line, ' ')[2] - if startswith(path, "/stream/watch") + if startswith(path, "/stream/custom-watch") + write(socket, chunked_head("application/json; stream=watch")) + flush(socket) + send_chunk(socket, """{"kind":"ADDED","seq":10}\n""") + send_chunk(socket, """{"kind":"DELETED","seq":11}""") + finish_chunks(socket) + elseif startswith(path, "/stream/watch") write(socket, chunked_head("application/json")) flush(socket) # one item split across two chunks, then two more items @@ -1061,6 +1144,39 @@ end @test [(item.kind, item.seq) for item in watch_items] == [("ADDED", 1), ("MODIFIED", 2), ("DELETED", 3)] + invalid_custom_channel = Channel{Any}(16) + call( + :customwatchstream; + client = raw_client, + stream_to = invalid_custom_channel, + ) + @test_throws C.SchemaValidationError take!(invalid_custom_channel) + + C.codec!( + raw_client, + "Application/JSON; STREAM=watch"; + decode = (bytes, media) -> begin + value = JSON.parse(String(bytes)) + return (; kind = value["kind"], seq = value["seq"], media) + end, + ) + custom_channel = Channel{Any}(16) + call(:customwatchstream; client = raw_client, stream_to = custom_channel) + custom_items = collect(custom_channel) + @test [(item.kind, item.seq) for item in custom_items] == + [("ADDED", 10), ("DELETED", 11)] + @test all( + item -> item.media == "application/json; stream=watch", + custom_items, + ) + + # The parameter-specific codec must not replace the normal + # application/json decoder. + second_watch_channel = Channel{Any}(16) + call(:watchstream; client = raw_client, stream_to = second_watch_channel) + @test [(item.kind, item.seq) for item in collect(second_watch_channel)] == + [("ADDED", 1), ("MODIFIED", 2), ("DELETED", 3)] + log_channel = Channel{Any}(16) @test call(:logstream; client = raw_client, stream_to = log_channel) === log_channel From ae201a7334c7a807c4c38dac85b553f2c4565fb2 Mon Sep 17 00:00:00 2001 From: Jacob Quinn Date: Mon, 10 Aug 2026 05:50:05 -0600 Subject: [PATCH 8/9] refactor(client)!: import the generated runtime from OpenAPI.Runtime Generated clients and server stubs previously carried a pasted copy of the ~2,000-line protocol runtime, while also depending on OpenAPI for the schema engine. Promote those templates into a real OpenAPI.Runtime module that generated modules import, so a generated client now contains only its own document data, models, and typed operations (the single-endpoint corpus case drops from ~2,400 to ~450 lines). Document-specific data moves into Runtime.Spec: each generated module packages its schema resources, security schemes, and default server into a `_SPEC` constant that its Client values carry, keeping schema-graph caches and server overrides isolated per module. Public conveniences (Client(), server!, credential!, ...) are emitted as module-local forwarders rather than methods on Runtime generics, so independently generated modules can never clobber each other. Models still extend Runtime._decode/_encode/_form_fields with methods on their own types. The HTTP transport core (_request, _stream_request and stream pumping) lives in OpenAPIHTTPExt as methods on Runtime stubs, preserving OpenAPI's weak HTTP dependency; generated clients keep `using HTTP`, which loads the extension. The zoned date-time codecs move to a new OpenAPITimeZonesExt loaded the same way by `datetime = :zoned` modules. BREAKING: generated-module `Client` is now a builder function returning Runtime.Client rather than a module-local struct type, and runtime internals are no longer defined inside generated modules. Co-Authored-By: Claude Fable 5 --- Project.toml | 7 + ext/OpenAPIHTTPExt.jl | 360 +++++ ext/OpenAPITimeZonesExt.jl | 40 + src/client.jl | 2669 +---------------------------------- src/runtime.jl | 2275 ++++++++++++++++++++++++++++- src/servergen.jl | 66 +- test/discriminators.jl | 2 +- test/models.jl | 12 +- test/normalization.jl | 20 +- test/references.jl | 6 +- test/runtime.jl | 8 +- test/runtime_integration.jl | 7 +- 12 files changed, 2832 insertions(+), 2640 deletions(-) create mode 100644 ext/OpenAPITimeZonesExt.jl diff --git a/Project.toml b/Project.toml index f15fc5c..5d9b623 100644 --- a/Project.toml +++ b/Project.toml @@ -7,22 +7,29 @@ authors = ["JuliaHub Inc."] version = "1.0.0" [deps] +Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" +UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" [weakdeps] HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" +TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" [extensions] OpenAPIHTTPExt = "HTTP" +OpenAPITimeZonesExt = "TimeZones" [compat] +Base64 = "1.11" Dates = "1.11" HTTP = "2" JSON = "1.7" +TimeZones = "1" URIs = "1" +UUIDs = "1.11" YAML = "0.4" julia = "1.11" diff --git a/ext/OpenAPIHTTPExt.jl b/ext/OpenAPIHTTPExt.jl index ca4e0e9..fa8a501 100644 --- a/ext/OpenAPIHTTPExt.jl +++ b/ext/OpenAPIHTTPExt.jl @@ -178,4 +178,364 @@ OpenAPI.server_source(::Val{:HTTP}, plan::OpenAPI.ServerPlan) = glue = GENERATED_HTTP_SERVER_GLUE, ) +# ---- Generated-client HTTP transport (methods on OpenAPI.Runtime stubs) ---- +import OpenAPI.Runtime: + ABSENT, + Absent, + ApiError, + ApiResponse, + Client, + _append_parameter!, + _base_media_type, + _decode_response_headers, + _decode_stream_item, + _drain_stream_buffer!, + _encode, + _encode_body, + _escape, + _finish_buffered_response, + _header_values, + _safe_header, + _security!, + _select_media, + _select_response, + _selected_media_entry, + _server_for, + _set_header!, + _stream_plan, + _validate_schema +import OpenAPI.Runtime: _request, _stream_request, _pump_stream!, _abort_stream_on_close! + +function _request( + client::Client, + operation, + values::Dict{Symbol,Any}; + body = ABSENT, + content_type = nothing, + accept = nothing, + with_http_info::Bool = false, + request_headers = Pair{String,String}[], + request_options::NamedTuple = NamedTuple(), + multipart_headers = NamedTuple(), + stream_to::Union{Nothing,Channel} = nothing, +) + path = operation.path + query = Tuple{String,String,Bool,Bool}[] + headers = Pair{String,String}[_safe_header(key, value) for (key, value) in client.headers] + for (key, value) in request_headers + _set_header!(headers, key, value) + end + cookies = Tuple{String,String,Bool,Bool}[] + for descriptor in operation.parameters + value = get(values, descriptor.arg, ABSENT) + if value isa Absent + descriptor.required && + throw(ArgumentError("required parameter $(descriptor.name) is missing")) + continue + end + client.validate_requests && _validate_schema(client.spec, + descriptor.schema, + _encode(value), + "encoding parameter $(descriptor.name)"; + direction = :input, + ) + path = _append_parameter!( + client, + path, + query, + headers, + cookies, + descriptor, + value, + ) + end + occursin('{', path) && + throw(ArgumentError("not all path template parameters were supplied for $(operation.id)")) + default_options = stream_to isa Channel ? (; protocol = :h1) : NamedTuple() + base_options = merge(default_options, client.request_options, request_options) + options = _security!( + client, + operation.security, + query, + headers, + cookies, + base_options, + ) + if !isempty(cookies) + cookie_text = join( + ( + fragment ? key : + (raw ? key : _escape(key)) * "=" * + (raw ? value : _escape(value)) for + (key, value, raw, fragment) in cookies + ), + "; ", + ) + existing = join(_header_values(headers, "Cookie"), "; ") + _set_header!( + headers, + "Cookie", + isempty(existing) ? cookie_text : existing * "; " * cookie_text, + ) + options = merge(options, (; cookies = false)) + end + query_text = isempty(query) ? "" : "?" * join( + ( + _escape(key) * "=" * + (preencoded ? value : _escape(value; allow_reserved)) for + (key, value, allow_reserved, preencoded) in query + ), + '&', + ) + url = _server_for(client, operation) * + (startswith(path, '/') ? path : "/" * path) * query_text + + payload = UInt8[] + if operation.request !== nothing && !(body isa Absent) + media = operation.request.media + entry = content_type === nothing ? first(media) : + _select_media(media, String(content_type)) + entry === nothing && throw( + ArgumentError("unsupported request Content-Type $(repr(content_type)) for $(operation.id)"), + ) + client.validate_requests && _validate_schema(client.spec, + entry[3], + _encode(body), + "encoding the $(operation.id) request body"; + direction = :input, + ) + payload, actual_content_type = + _encode_body( + client, + body, + entry[1], + entry[4]; + multipart_headers, + ) + _set_header!(headers, "Content-Type", actual_content_type) + elseif operation.request !== nothing && operation.request.required + throw(ArgumentError("required request body is missing for $(operation.id)")) + end + documented_accept = String[ + entry[1] for response in operation.responses for entry in response.media + if startswith(uppercase(response.selector), "2") || + uppercase(response.selector) == "DEFAULT" + ] + existing_accept = _header_values(headers, "Accept") + selected_accept = if accept !== nothing + String(accept) + elseif !isempty(existing_accept) + nothing + else + join(unique(documented_accept), ", ") + end + selected_accept === nothing || isempty(selected_accept) || + _set_header!(headers, "Accept", selected_accept) + + stream_to === nothing || return _stream_request( + client, + operation, + url, + headers, + payload, + options, + stream_to, + with_http_info, + ) + options = merge(options, (body = payload, status_exception = false)) + response = HTTP.request( + operation.method, + url, + headers; + options..., + ) + response_headers = Pair{String,String}[ + String(key) => String(value) for (key, value) in response.headers + ] + return _finish_buffered_response( + client, + operation, + Int(response.status), + response_headers, + HTTP.header(response, "Content-Type", ""), + Vector{UInt8}(response.body), + with_http_info, + ) +end + +function _pump_stream!( + client::Client, + stream, + channel::Channel, + kind::Symbol, + item_type, + schema, + media_type, + finished, +) + buffer = UInt8[] + try + while !eof(stream) + chunk = readavailable(stream) + isempty(chunk) && continue + if kind === :bytes + put!( + channel, + _decode_stream_item( + client, + chunk, + kind, + item_type, + schema, + media_type, + ), + ) + else + append!(buffer, chunk) + _drain_stream_buffer!( + client, + channel, + buffer, + kind, + item_type, + schema, + media_type, + false, + ) + end + end + kind === :bytes || _drain_stream_buffer!( + client, + channel, + buffer, + kind, + item_type, + schema, + media_type, + true, + ) + finished[] = true + close(channel) + catch error + # A channel the consumer closed is the abort signal; anything else is + # delivered to the consumer through the channel. + if isopen(channel) + finished[] = true + close(channel, error) + end + finally + try + HTTP.closeread(stream) + catch + end + finished[] = true + end + return nothing +end + +function _abort_stream_on_close!(stream, channel::Channel, producer::Task, finished) + while isopen(channel) && !finished[] + sleep(0.25) + end + finished[] && return nothing + try + HTTP.closeread(stream) + catch + end + for _ in 1:4 + istaskdone(producer) && return nothing + sleep(0.25) + end + try + close(stream) + catch + end + return nothing +end + +function _stream_request( + client::Client, + operation, + url, + headers, + payload::Vector{UInt8}, + options, + stream_to::Channel, + with_http_info::Bool, +) + stream = HTTP.open(operation.method, url, headers; options...) + local response + try + isempty(payload) || write(stream, payload) + HTTP.closewrite(stream) + response = HTTP.startread(stream) + catch + try + HTTP.closeread(stream) + catch + end + rethrow() + end + response_headers = Pair{String,String}[ + String(key) => String(value) for (key, value) in response.headers + ] + received = HTTP.header(response, "Content-Type", "") + status = Int(response.status) + if !(200 <= status < 300) + bytes = try + read(stream) + finally + try + HTTP.closeread(stream) + catch + end + end + # Throws ApiError with the fully decoded error body. + return _finish_buffered_response( + client, + operation, + status, + response_headers, + received, + bytes, + with_http_info, + ) + end + local descriptor, decoded_headers, kind, item_type, schema, actual_media + try + descriptor = _select_response(operation.responses, status) + decoded_headers = _decode_response_headers(client, descriptor, response_headers) + if descriptor === nothing || isempty(descriptor.media) + actual_media = isempty(received) ? "application/octet-stream" : String(received) + kind, item_type, schema = :bytes, Vector{UInt8}, nothing + else + selected, actual_media = + _selected_media_entry(operation.id, status, descriptor, received) + kind, item_type, schema = + _stream_plan(_base_media_type(actual_media), selected[2], selected[3]) + end + catch + try + HTTP.closeread(stream) + catch + end + rethrow() + end + finished = Ref(false) + producer = errormonitor(@async _pump_stream!( + client, + stream, + stream_to, + kind, + item_type, + schema, + actual_media, + finished, + )) + errormonitor(@async _abort_stream_on_close!(stream, stream_to, producer, finished)) + return with_http_info ? + ApiResponse(status, response_headers, decoded_headers, stream_to) : + stream_to +end + end # module diff --git a/ext/OpenAPITimeZonesExt.jl b/ext/OpenAPITimeZonesExt.jl new file mode 100644 index 0000000..6c822bd --- /dev/null +++ b/ext/OpenAPITimeZonesExt.jl @@ -0,0 +1,40 @@ +# TimeZones extension: zoned date-time codecs for generated OpenAPI modules +# emitted with `datetime = :zoned` (their `using TimeZones` loads this). +module OpenAPITimeZonesExt + +using TimeZones, Dates +import OpenAPI.Runtime: _decode, _encode, DecodeError + +function _decode(::Type{TimeZones.ZonedDateTime}, value::AbstractString) + matched = match( + r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})?$", + value, + ) + matched === nothing && throw(DecodeError("invalid RFC 3339 date-time $(repr(value))")) + try + clock = Dates.DateTime(matched.captures[1]) + fraction = matched.captures[2] + if fraction !== nothing + clock += Dates.Millisecond( + parse(Int, rpad(first(fraction, min(3, length(fraction))), 3, '0')), + ) + end + zone = matched.captures[3] + timezone = if zone === nothing || zone == "Z" + TimeZones.tz"UTC" + else + sign = startswith(zone, '+') ? 1 : -1 + hours = parse(Int, zone[2:3]) + minutes = parse(Int, zone[5:6]) + TimeZones.FixedTimeZone(zone, sign * (3600 * hours + 60 * minutes)) + end + return TimeZones.ZonedDateTime(clock, timezone) + catch error + error isa DecodeError && rethrow() + throw(DecodeError("invalid RFC 3339 date-time: $(sprint(showerror, error))")) + end +end +_encode(value::TimeZones.ZonedDateTime) = + Dates.format(value, TimeZones.ISOZonedDateTimeFormat) + +end # module OpenAPITimeZonesExt diff --git a/src/client.jl b/src/client.jl index 358eff9..2930c2d 100644 --- a/src/client.jl +++ b/src/client.jl @@ -1,2581 +1,58 @@ -const GENERATED_RUNTIME_COMMON = raw""" -struct Absent end -const ABSENT = Absent() -Base.show(io::IO, ::Absent) = print(io, "ABSENT") - -struct DecodeError <: Exception - message::String -end -Base.showerror(io::IO, error::DecodeError) = print(io, error.message) - -struct UnsupportedMediaType <: Exception - media_type::String - direction::Symbol -end -Base.showerror(io::IO, error::UnsupportedMediaType) = print( - io, - "no ", - error.direction, - " codec is configured for media type ", - repr(error.media_type), -) - -struct SchemaValidationError <: Exception - context::String - issues::Vector{Any} -end -function Base.showerror(io::IO, error::SchemaValidationError) - print(io, "schema validation failed while ", error.context) - for issue in error.issues - print(io, "\n ", issue.path, ": ", issue.reason) - end -end - -const _SCHEMA_GRAPHS = Dict{Symbol,Any}() -const _SCHEMA_GRAPH_LOCK = ReentrantLock() - -function _schema_graph(direction::Symbol = :neutral) - direction in (:neutral, :input, :output) || - throw(ArgumentError("schema direction must be :neutral, :input, or :output")) - isempty(_SCHEMA_ROOT_DATA) && return nothing - haskey(_SCHEMA_GRAPHS, direction) && return _SCHEMA_GRAPHS[direction] - return lock(_SCHEMA_GRAPH_LOCK) do - haskey(_SCHEMA_GRAPHS, direction) && return _SCHEMA_GRAPHS[direction] - documents = Dict{String,Any}( - entry.id => JSON.parse(entry.json; duplicate_keys = :error) for - entry in _SCHEMA_RESOURCE_DATA - ) - if direction !== :neutral - for rule in _SCHEMA_DIRECTIONAL_REQUIRED - removed = direction === :input ? rule.input : rule.output - isempty(removed) && continue - document = get(documents, rule.resource, nothing) - document === nothing && continue - schema = SchemaEngine.Resources.resolve( - document, - SchemaEngine.Resources.JSONPointer(rule.pointer), - ) - schema isa AbstractDict || continue - required = get(schema, "required", nothing) - required isa AbstractVector || continue - retained = Any[ - name for name in required if String(name) ∉ removed - ] - isempty(retained) ? delete!(schema, "required") : - (schema["required"] = retained) - end - end - resources = SchemaEngine.Resources.Resource[] - for entry in _SCHEMA_RESOURCE_DATA - id = SchemaEngine.Resources.ResourceId(entry.id) - retrieval = SchemaEngine.Resources.ResourceId(entry.retrieval) - push!( - resources, - SchemaEngine.Resources.Resource( - id, - documents[entry.id]; - retrieval, - media_type = entry.media_type, - ), - ) - end - roots = SchemaEngine.Resources.NodeId[ - SchemaEngine.Resources.NodeId( - SchemaEngine.Resources.ResourceId(entry.resource), - SchemaEngine.Resources.JSONPointer(entry.pointer), - ) for entry in _SCHEMA_ROOT_DATA - ] - root_dialects = Dict( - root => entry.dialect for (root, entry) in zip(roots, _SCHEMA_ROOT_DATA) - ) - dialect_aliases = Dict( - entry.uri => SchemaEngine.Dialect( - entry.name, - entry.uri, - entry.id_keyword, - entry.ref_siblings, - entry.modern_items, - entry.unevaluated, - entry.dynamic_refs, - entry.recursive_refs, - entry.applicator, - entry.validation, - ) for entry in _SCHEMA_DIALECT_DATA - ) - graph = SchemaEngine.CompiledSchemas( - resources, - roots; - dialect = SchemaEngine.DRAFT202012, - root_dialects, - dialect_aliases, - ) - _SCHEMA_GRAPHS[direction] = graph - return graph - end -end - -function _schema_at(descriptor, direction::Symbol = :neutral) - descriptor === nothing && return nothing - graph = _schema_graph(direction) - graph === nothing && return nothing - node = SchemaEngine.Resources.NodeId( - SchemaEngine.Resources.ResourceId(descriptor.resource), - SchemaEngine.Resources.JSONPointer(descriptor.pointer), - ) - return SchemaEngine.subschema(graph, node) -end - -function _schema_issues(descriptor, value; direction::Symbol = :neutral) - schema = _schema_at(descriptor, direction) - schema === nothing && return Any[] - return Any[SchemaEngine.validate(schema, value; fail_fast = false)...] -end - -function _validate_schema( - descriptor, - value, - context; - direction::Symbol = :neutral, -) - issues = _schema_issues(descriptor, value; direction) - isempty(issues) || throw(SchemaValidationError(String(context), issues)) - return value -end - -_schema_valid(descriptor, value; direction::Symbol = :neutral) = - isempty(_schema_issues(descriptor, value; direction)) - -struct Upload - data::Vector{UInt8} - filename::Union{Nothing,String} - content_type::Union{Nothing,String} - headers::Vector{Pair{String,String}} -end - -# Header values for one multipart part and, when needed, its nested parts. -struct MultipartPartHeaders - values::Any - parts::Any -end -MultipartPartHeaders(values = NamedTuple(); parts = NamedTuple()) = - MultipartPartHeaders(values, parts) -function Upload( - data::AbstractVector{UInt8}; - filename::Union{Nothing,AbstractString} = nothing, - content_type::Union{Nothing,AbstractString} = nothing, - headers = Pair{String,String}[], -) - return Upload( - Vector{UInt8}(data), - filename === nothing ? nothing : String(filename), - content_type === nothing ? nothing : String(content_type), - Pair{String,String}[String(key) => String(value) for (key, value) in headers], - ) -end - -_typename(::Type{T}) where {T} = string(T) -_required(value, key, model) = haskey(value, key) ? value[key] : - throw(DecodeError("required field $(repr(key)) is missing while decoding $model")) -_object(value, model) = value isa AbstractDict ? value : - throw(DecodeError("expected an object while decoding $model, got $(typeof(value))")) - -_decode(::Type{Any}, value) = value -_decode(::Type{Nothing}, ::Nothing) = nothing -_decode(::Type{Nothing}, value) = throw(DecodeError("expected null, got $(typeof(value))")) -_decode(::Type{String}, value::AbstractString) = String(value) -_decode(::Type{Bool}, value::Bool) = value -_decode(::Type{T}, value::Integer) where {T<:Integer} = try - value isa Bool && throw(DecodeError("expected an integer, got Bool")) - convert(T, value) -catch - throw(DecodeError("integer $value does not fit $T")) -end -function _decode(::Type{T}, value::Real) where {T<:AbstractFloat} - value isa Bool && throw(DecodeError("expected a number, got Bool")) - return convert(T, value) -end -function _decode(::Type{Dates.Date}, value::AbstractString) - try - return Dates.Date(value) - catch error - throw(DecodeError("invalid RFC 3339 full-date: $(sprint(showerror, error))")) - end -end -function _decode(::Type{Dates.Time}, value::AbstractString) - try - return Dates.Time(value) - catch error - throw(DecodeError("invalid time: $(sprint(showerror, error))")) - end -end -function _decode(::Type{Dates.DateTime}, value::AbstractString) - # RFC 3339 requires the offset, but zone-less ISO 8601 date-times are - # widespread in deployed APIs (most JSON serializers print naive - # timestamps). Accept input liberally: a missing offset means UTC, the - # same convention _encode uses when it stamps naive DateTimes with `Z`. - matched = match( - r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})?$", - value, - ) - matched === nothing && throw(DecodeError("invalid RFC 3339 date-time $(repr(value))")) - try - output = Dates.DateTime(matched.captures[1]) - fraction = matched.captures[2] - if fraction !== nothing - milliseconds = parse(Int, rpad(first(fraction, min(3, length(fraction))), 3, '0')) - output += Dates.Millisecond(milliseconds) - end - zone = matched.captures[3] - if zone !== nothing && zone != "Z" - sign = startswith(zone, '+') ? 1 : -1 - hours = parse(Int, zone[2:3]) - minutes = parse(Int, zone[5:6]) - output -= Dates.Minute(sign * (60 * hours + minutes)) - end - return output - catch error - error isa DecodeError && rethrow() - throw(DecodeError("invalid RFC 3339 date-time: $(sprint(showerror, error))")) - end -end -function _decode(::Type{UUIDs.UUID}, value::AbstractString) - try - return UUIDs.UUID(value) - catch error - throw(DecodeError("invalid UUID: $(sprint(showerror, error))")) - end -end -function _decode(::Type{Vector{UInt8}}, value::AbstractString) - try - return Base64.base64decode(value) - catch error - throw(DecodeError("invalid base64 data: $(sprint(showerror, error))")) - end -end -_decode(::Type{Vector{UInt8}}, value::AbstractVector{UInt8}) = copy(value) - -_decode(::Type{T}, value::AbstractVector) where {T<:AbstractVector} = - _decode(T, value, true) -function _decode(::Type{T}, value::AbstractVector, validate::Bool) where {T<:AbstractVector} - element = eltype(T) - return T([_decode(element, item, validate) for item in value]) -end -_decode(::Type{T}, value::AbstractDict) where {T<:AbstractDict} = - _decode(T, value, true) -function _decode(::Type{T}, value::AbstractDict, validate::Bool) where {T<:AbstractDict} - keytype(T) <: AbstractString || - throw(DecodeError("only string-key dictionaries are supported, got $T")) - output = T() - for (key, item) in value - output[convert(keytype(T), key)] = _decode(valtype(T), item, validate) - end - return output -end -_decode(::Type{T}, value::AbstractVector) where {T<:Tuple} = - _decode(T, value, true) -function _decode(::Type{T}, value::AbstractVector, validate::Bool) where {T<:Tuple} - length(value) == fieldcount(T) || - throw(DecodeError("expected $(fieldcount(T)) tuple items, got $(length(value))")) - return T(( - _decode(fieldtype(T, index), value[index], validate) for - index in 1:fieldcount(T) - )...) -end - -function _direct_union_match(::Type{T}, value) where {T} - T === Nothing && return value === nothing - T === Bool && return value isa Bool - T <: Integer && return value isa Integer && !(value isa Bool) - T <: AbstractFloat && return value isa AbstractFloat - T <: AbstractString && return value isa AbstractString - T <: AbstractVector && return value isa AbstractVector - T <: AbstractDict && return value isa AbstractDict - return value isa T -end - -function _decode_union( - ::Type{T}, - value; - oneof::Bool = false, - validate::Bool = true, -) where {T} - variants = Base.uniontypes(T) - if value === nothing && Nothing in variants - return nothing - end - if !oneof - preferred = Any[ - variant for variant in variants - if variant ∉ (Absent, Nothing) && _direct_union_match(variant, value) - ] - append!(preferred, Any[variant for variant in variants if variant ∉ preferred]) - variants = preferred - end - successes = Any[] - failures = String[] - for variant in variants - variant in (Absent, Nothing) && continue - try - decoded = _decode(variant, value, validate) - oneof || return decoded - push!(successes, decoded) - catch error - error isa DecodeError || rethrow() - push!(failures, error.message) - end - end - length(successes) == 1 && return only(successes) - isempty(successes) && throw( - DecodeError("value does not match any variant of $T: " * join(failures, "; ")), - ) - throw(DecodeError("value matches more than one variant of $T")) -end - -function _decode(::Type{T}, value) where {T} - T isa Union && return _decode_union(T, value) - value isa T && return value - try - return convert(T, value) - catch - throw(DecodeError("cannot decode $(typeof(value)) as $T")) - end -end - -function _decode(::Type{T}, value, validate::Bool) where {T} - T isa Union && return _decode_union(T, value; validate) - return _decode(T, value) -end - -_encode(::Absent) = throw(ArgumentError("ABSENT is only valid as an object field")) -_encode(::Nothing) = nothing -_encode(value::Union{Bool,Number,AbstractString}) = value -_encode(value::Union{Dates.Date,Dates.Time,UUIDs.UUID}) = string(value) -_encode(value::Dates.DateTime) = Dates.format(value, dateformat"yyyy-mm-ddTHH:MM:SS.sss") * "Z" -_encode(value::AbstractVector{UInt8}) = Base64.base64encode(value) -_encode(value::Upload) = Base64.base64encode(value.data) -_encode(value::AbstractVector) = Any[_encode(item) for item in value] -_encode(value::Tuple) = Any[_encode(item) for item in value] -function _encode(value::AbstractDict) - output = JSON.Object{String,Any}() - for (key, item) in value - output[String(key)] = _encode(item) - end - return output -end -function _encode(value::NamedTuple) - output = JSON.Object{String,Any}() - for (key, item) in pairs(value) - output[String(key)] = _encode(item) - end - return output -end -_encode(value) = value - -function _safe_header(name, value) - header = String(name) - content = String(value) - occursin(r"^[!#$%&'*+.^_`|~0-9A-Za-z-]+$", header) || - throw(ArgumentError("invalid HTTP header name $(repr(header))")) - (occursin('\r', content) || occursin('\n', content)) && - throw(ArgumentError("HTTP header values must not contain CR or LF")) - return header => content -end - -function _set_header!(headers, name, value) - pair = _safe_header(name, value) - lowered = lowercase(pair.first) - filter!(entry -> lowercase(entry.first) != lowered, headers) - push!(headers, pair) - return headers -end - -function _media_segments(value) - output = String[] - io = IOBuffer() - quoted = false - escaped = false - for char in String(value) - if escaped - write(io, char) - escaped = false - elseif quoted && char == '\\' - write(io, char) - escaped = true - elseif char == '"' - write(io, char) - quoted = !quoted - elseif char == ';' && !quoted - push!(output, String(take!(io))) - else - write(io, char) - end - end - push!(output, String(take!(io))) - return output -end - -function _media_parameter_value(value) - text = strip(String(value)) - startswith(text, '"') && endswith(text, '"') && ncodeunits(text) >= 2 || - return text - ncodeunits(text) == 2 && return "" - io = IOBuffer() - escaped = false - for char in SubString(text, nextind(text, firstindex(text)), prevind(text, lastindex(text))) - if escaped - write(io, char) - escaped = false - elseif char == '\\' - escaped = true - else - write(io, char) - end - end - escaped && write(io, '\\') - return String(take!(io)) -end - -function _media_type(value) - segments = _media_segments(value) - base = lowercase(strip(first(segments))) - parameters = Dict{String,String}() - for segment in Iterators.drop(segments, 1) - pair = split(segment, '='; limit = 2) - length(pair) == 2 || continue - name = lowercase(strip(pair[1])) - isempty(name) && continue - parameter = _media_parameter_value(pair[2]) - parameters[name] = name == "charset" ? lowercase(parameter) : parameter - end - return base, parameters -end - -function _media_match_score(received::String, documented::String) - received_base = first(_media_type(received)) - documented_base = first(_media_type(documented)) - received_base == documented_base && return 4 - documented_base == "*/*" && return 1 - parts = split(documented_base, '/'; limit = 2) - received_parts = split(received_base, '/'; limit = 2) - length(parts) == 2 && length(received_parts) == 2 || return 0 - parts[1] == received_parts[1] || parts[1] == "*" || return 0 - parts[2] == "*" && return parts[1] == "*" ? 1 : 2 - startswith(parts[2], "*+") && - endswith(received_parts[2], parts[2][2:end]) && return 3 - return 0 -end - -function _media_selection_score(received::String, documented::String) - received_base, received_parameters = _media_type(received) - documented_base, documented_parameters = _media_type(documented) - base_score = _media_match_score(received_base, documented_base) - base_score > 0 || return (0, 0, 0) - all(documented_parameters) do (name, value) - return get(received_parameters, name, nothing) == value - end || return (0, 0, 0) - exact = length(received_parameters) == length(documented_parameters) - return (base_score, length(documented_parameters), exact ? 1 : 0) -end - -_media_match(received::String, documented::String) = - first(_media_selection_score(received, documented)) > 0 - -_base_media_type(value) = lowercase(strip(first(split(String(value), ';'; limit = 2)))) - -function _select_media(media, received) - isempty(media) && return nothing - selected = nothing - score = (0, 0, 0) - for entry in media - candidate = _media_selection_score(String(received), String(entry[1])) - if candidate > score - selected = entry - score = candidate - end - end - return selected -end - -function _select_media_codec(codecs, received) - selected = nothing - score = (0, 0, 0) - for (documented, codec) in codecs - candidate = _media_selection_score(String(received), documented) - if candidate > score - selected = codec - score = candidate - end - end - return selected -end - -function _store_media_codec!(codecs, media_type, codec) - key = strip(String(media_type)) - parsed = _media_type(key) - for existing in collect(keys(codecs)) - _media_type(existing) == parsed && delete!(codecs, existing) - end - codecs[key] = codec - return codecs -end - -function _select_response(responses, status) - exact = string(status) - range = string(div(status, 100), "XX") - default = nothing - for response in responses - selector = uppercase(response.selector) - selector == exact && return response - selector == range && (default = response) - selector == "DEFAULT" && default === nothing && (default = response) - end - return default -end - -function _header_values(headers, name) - lowered = lowercase(String(name)) - return String[ - value for (key, value) in headers if lowercase(String(key)) == lowered - ] -end - -function _header_atom(value) - text = String(value) - lowered = lowercase(text) - lowered == "true" && return true - lowered == "false" && return false - lowered == "null" && return nothing - integer = tryparse(Int64, text) - integer === nothing || return integer - number = tryparse(Float64, text) - number === nothing || return number - return text -end - -function _header_scalar(type, value) - text = String(value) - type === String && return text - type === Bool && return lowercase(text) == "true" ? true : - lowercase(text) == "false" ? false : - throw(DecodeError("invalid boolean value $(repr(text))")) - type <: Integer && return try - parse(type, text) - catch error - throw(DecodeError("invalid integer value: $(sprint(showerror, error))")) - end - type <: AbstractFloat && return try - parse(type, text) - catch error - throw(DecodeError("invalid numeric value: $(sprint(showerror, error))")) - end - return _decode(type, text) -end - -function _header_type_variant(type, shape) - variants = type isa Union ? Base.uniontypes(type) : (type,) - for variant in variants - variant in (Nothing, Absent) && continue - shape === :array && variant <: AbstractVector && return variant - shape === :object && - (variant <: AbstractDict || isstructtype(variant)) && return variant - shape === :scalar && return variant - end - return type -end - -function _decode_schema_header( - type, - values, - shape, - explode, - schema; - set_cookie::Bool = false, - direction::Symbol = :output, - context = "decoding a response header", - validate::Bool = true, -) - selected = _header_type_variant(type, shape) - raw = if shape === :array - items = if set_cookie - String[String(value) for value in values] - else - output = String[] - for value in values - append!(output, split(value, ',')) - end - output - end - element = selected <: AbstractVector ? eltype(selected) : String - Any[_header_scalar(element, item) for item in items] - elseif shape === :object - object = JSON.Object{String,Any}() - if set_cookie && explode - for value in values - pair = split(value, '='; limit = 2) - length(pair) == 2 || throw( - DecodeError("invalid Set-Cookie response header"), - ) - object[pair[1]] = pair[2] - end - elseif explode - tokens = split(join(values, ','), ',') - for token in tokens - pair = split(token, '='; limit = 2) - length(pair) == 2 || throw( - DecodeError("invalid exploded object response header"), - ) - object[pair[1]] = _header_atom(pair[2]) - end - else - tokens = split(join(values, ','), ',') - iseven(length(tokens)) || - throw(DecodeError("invalid object response header")) - for index in 1:2:length(tokens) - object[tokens[index]] = _header_atom(tokens[index + 1]) - end - end - object - else - _header_scalar(selected, join(values, set_cookie ? '\n' : ',')) - end - validate && _validate_schema( - schema, - _encode(raw), - context; - direction, - ) - return _decode(type, raw, validate) -end - -_is_json_media(media) = media == "application/json" || endswith(media, "+json") -_is_sequential_json_media(media) = media in ( - "application/jsonl", - "application/x-ndjson", - "application/json-seq", - "application/geo+json-seq", -) || endswith(media, "+json-seq") - -function _parse_json(text, context) - source = text isa AbstractString ? String(text) : String(copy(text)) - isvalid(source) || throw(DecodeError("invalid UTF-8 while $context")) - try - return JSON.parse(source; duplicate_keys = :error) - catch error - throw(DecodeError("invalid JSON while $context: $(sprint(showerror, error))")) - end -end - -function _decode_sequential_json(body, media) - records = Any[] - if media == "application/json-seq" || endswith(media, "+json-seq") - for chunk in split(String(body), Char(0x1e)) - text = strip(chunk) - isempty(text) || push!(records, _parse_json(text, "decoding a JSON sequence")) - end - else - for line in eachline(IOBuffer(body)) - text = strip(line) - isempty(text) || push!(records, _parse_json(text, "decoding JSON lines")) - end - end - return records -end - -function _encode_sequential_json(value, media) - encoded = _encode(value) - encoded isa AbstractVector || encoded isa Tuple || throw( - ArgumentError("sequential JSON request bodies must be arrays or tuples"), - ) - if media == "application/json-seq" || endswith(media, "+json-seq") - return join((string(Char(0x1e), JSON.json(item), '\n') for item in encoded)) - end - return join((JSON.json(item) * "\n" for item in encoded)) -end -""" - -# Emitted only for `datetime = :zoned` plans, directly after -# GENERATED_RUNTIME_COMMON, in both generated clients and servers. -const GENERATED_ZONED_RUNTIME = raw""" -function _decode(::Type{TimeZones.ZonedDateTime}, value::AbstractString) - matched = match( - r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})?$", - value, - ) - matched === nothing && throw(DecodeError("invalid RFC 3339 date-time $(repr(value))")) - try - clock = Dates.DateTime(matched.captures[1]) - fraction = matched.captures[2] - if fraction !== nothing - clock += Dates.Millisecond( - parse(Int, rpad(first(fraction, min(3, length(fraction))), 3, '0')), - ) - end - zone = matched.captures[3] - timezone = if zone === nothing || zone == "Z" - TimeZones.tz"UTC" - else - sign = startswith(zone, '+') ? 1 : -1 - hours = parse(Int, zone[2:3]) - minutes = parse(Int, zone[5:6]) - TimeZones.FixedTimeZone(zone, sign * (3600 * hours + 60 * minutes)) - end - return TimeZones.ZonedDateTime(clock, timezone) - catch error - error isa DecodeError && rethrow() - throw(DecodeError("invalid RFC 3339 date-time: $(sprint(showerror, error))")) - end -end -_encode(value::TimeZones.ZonedDateTime) = - Dates.format(value, TimeZones.ISOZonedDateTimeFormat) -""" - -const GENERATED_RUNTIME = raw""" -struct ApiError <: Exception - operation_id::String - status::Int - headers::Vector{Pair{String,String}} - decoded_headers::Dict{String,Any} - body::Vector{UInt8} - decoded::Any - decode_error::Any -end -function Base.showerror(io::IO, error::ApiError) - print(io, "ApiError(", error.status, ") in ", error.operation_id) - if error.decode_error !== nothing - print(io, " (response decoding failed: ") - showerror(io, error.decode_error) - print(io, ')') - end - isempty(error.body) && return - text = isvalid(String, error.body) ? String(copy(error.body)) : nothing - if text === nothing - preview = bytes2hex(error.body[1:min(length(error.body), 32)]) - print(io, ": ", length(error.body), " binary bytes (", preview) - length(error.body) > 32 && print(io, "…") - print(io, ')') - else - preview = first(text, min(length(text), 512)) - print(io, ": ", preview) - length(text) > 512 && print(io, "…") - end -end - -struct ApiResponse{T} - status::Int - headers::Vector{Pair{String,String}} - decoded_headers::Dict{String,Any} - body::T -end - -struct UnexpectedBody <: Exception - operation_id::String - status::Int - bytes::Int -end -Base.showerror(io::IO, error::UnexpectedBody) = print( - io, - "operation ", - error.operation_id, - " received an undocumented ", - error.bytes, - "-byte response body for status ", - error.status, -) - -struct UnexpectedContentType <: Exception - operation_id::String - status::Int - received::String - expected::Tuple -end -function Base.showerror(io::IO, error::UnexpectedContentType) - print( - io, - "unexpected Content-Type ", - repr(error.received), - " for ", - error.operation_id, - " response ", - error.status, - "; expected ", - join(error.expected, ", "), - ) -end - -abstract type AbstractCredential end -struct ApiKeyCredential <: AbstractCredential - value::String - authorizations::Set{String} -end -ApiKeyCredential(value::AbstractString; roles = String[]) = - ApiKeyCredential(String(value), Set(String.(roles))) -struct BasicCredential <: AbstractCredential - username::String - password::String - authorizations::Set{String} -end -BasicCredential(username::AbstractString, password::AbstractString; roles = String[]) = - BasicCredential(String(username), String(password), Set(String.(roles))) -struct BearerCredential <: AbstractCredential - token::String - authorizations::Set{String} -end -function BearerCredential(token::AbstractString; scopes = String[], roles = String[]) - return BearerCredential( - String(token), - union(Set(String.(scopes)), Set(String.(roles))), - ) -end -struct HttpCredential <: AbstractCredential - value::String - authorizations::Set{String} -end -HttpCredential(value::AbstractString; roles = String[]) = - HttpCredential(String(value), Set(String.(roles))) -struct MutualTLSCredential <: AbstractCredential - request_options::NamedTuple - authorizations::Set{String} -end -MutualTLSCredential(request_options::NamedTuple; roles = String[]) = - MutualTLSCredential(request_options, Set(String.(roles))) - -mutable struct Client - server::Union{Nothing,String} - server_index::Int - server_name::Union{Nothing,String} - server_variables::Dict{String,String} - credentials::Dict{String,AbstractCredential} - headers::Vector{Pair{String,String}} - request_options::NamedTuple - require_credentials::Bool - media_encoders::Dict{String,Function} - media_decoders::Dict{String,Function} - validate_requests::Bool - validate_responses::Bool -end - -function _normalize_media_codecs(codecs::AbstractDict) - normalized = Dict{String,Function}() - for (name, codec) in codecs - _store_media_codec!(normalized, name, codec) - end - return normalized -end - -function Client( - server::Union{Nothing,AbstractString} = nothing; - server_index::Integer = 1, - server_name::Union{Nothing,AbstractString} = nothing, - server_variables::AbstractDict = Dict{String,String}(), - credentials::AbstractDict = Dict{String,AbstractCredential}(), - headers = Pair{String,String}[], - request_options::NamedTuple = NamedTuple(), - require_credentials::Bool = true, - media_encoders::AbstractDict = Dict{String,Function}(), - media_decoders::AbstractDict = Dict{String,Function}(), - validate_requests::Bool = true, - validate_responses::Bool = true, -) - server_index > 0 || throw(ArgumentError("server_index must be positive")) - normalized_credentials = credentials isa Dict{String,AbstractCredential} ? - copy(credentials) : - Dict{String,AbstractCredential}( - String(name) => credential for (name, credential) in credentials - ) - normalized_headers = headers isa Vector{Pair{String,String}} ? copy(headers) : - Pair{String,String}[ - String(name) => String(value) for (name, value) in headers - ] - return Client( - server === nothing ? nothing : String(rstrip(server, '/')), - Int(server_index), - server_name === nothing ? nothing : String(server_name), - server_variables isa Dict{String,String} ? copy(server_variables) : - Dict{String,String}( - String(name) => String(value) for (name, value) in server_variables - ), - normalized_credentials, - normalized_headers, - request_options, - require_credentials, - _normalize_media_codecs(media_encoders), - _normalize_media_codecs(media_decoders), - validate_requests, - validate_responses, - ) -end - -function credential!(client::Client, name::AbstractString, credential::AbstractCredential) - client.credentials[String(name)] = credential - return client -end -credential!(name::AbstractString, credential::AbstractCredential) = - credential!(DEFAULT_CLIENT, name, credential) - -function clearcredential!(client::Client, name::AbstractString) - delete!(client.credentials, String(name)) - return client -end -clearcredential!(name::AbstractString) = clearcredential!(DEFAULT_CLIENT, name) - -function server!(client::Client, server::Union{Nothing,AbstractString}) - client.server = server === nothing ? nothing : String(rstrip(server, '/')) - isdefined(@__MODULE__, :DEFAULT_CLIENT) && client === DEFAULT_CLIENT && - (SERVER[] = something(client.server, _DEFAULT_SERVER)) - return client -end -server!(server::Union{Nothing,AbstractString}) = server!(DEFAULT_CLIENT, server) - -function server_index!(client::Client, index::Integer) - index > 0 || throw(ArgumentError("server index must be positive")) - client.server_index = Int(index) - client.server_name = nothing - return client -end -server_index!(index::Integer) = server_index!(DEFAULT_CLIENT, index) - -function server_name!(client::Client, name::Union{Nothing,AbstractString}) - client.server_name = name === nothing ? nothing : String(name) - return client -end -server_name!(name::Union{Nothing,AbstractString}) = server_name!(DEFAULT_CLIENT, name) - -function server_variable!(client::Client, name::AbstractString, value::AbstractString) - client.server_variables[String(name)] = String(value) - return client -end -server_variable!(name::AbstractString, value::AbstractString) = - server_variable!(DEFAULT_CLIENT, name, value) - -function codec!( - client::Client, - media_type::AbstractString; - encode::Union{Nothing,Function} = nothing, - decode::Union{Nothing,Function} = nothing, -) - encode === nothing || - _store_media_codec!(client.media_encoders, media_type, encode) - decode === nothing || - _store_media_codec!(client.media_decoders, media_type, decode) - return client -end -codec!(media_type::AbstractString; kwargs...) = - codec!(DEFAULT_CLIENT, media_type; kwargs...) - -function authorization!(client::Client, token::Union{Nothing,AbstractString}) - names = String[ - name for (name, scheme) in _SECURITY_SCHEMES - if scheme.type in (:http_bearer, :oauth2, :openidconnect) - ] - isempty(names) && throw(ArgumentError("the OpenAPI document has no bearer-compatible security scheme")) - for name in names - if token === nothing - delete!(client.credentials, name) - else - client.credentials[name] = BearerCredential(String(token)) - end - end - return client -end -authorization!(token::Union{Nothing,AbstractString}) = authorization!(DEFAULT_CLIENT, token) - -_scalar(::Nothing) = "" -_scalar(value::Bool) = value ? "true" : "false" -_scalar(value::Dates.DateTime) = _encode(value) -_scalar(value::Union{Dates.Date,Dates.Time,UUIDs.UUID}) = string(value) -_scalar(value::AbstractVector{UInt8}) = Base64.base64encode(value) -_scalar(value) = string(value) - -function _pairs(value) - lowered = _encode(value) - lowered isa AbstractDict || throw(ArgumentError("parameter style requires an object value")) - return Pair{String,Any}[String(key) => item for (key, item) in lowered] -end - -function _join_array(value, delimiter) - value isa AbstractVector || value isa Tuple || - throw(ArgumentError("parameter style requires an array value")) - return join((_scalar(item) for item in value), delimiter) -end - -function _join_object(value, pair_delimiter, key_delimiter) - return join( - (string(_scalar(key), key_delimiter, _scalar(item)) for (key, item) in _pairs(value)), - pair_delimiter, - ) -end - -_path_scalar(value) = _escape(_scalar(value)) - -function _path_array(value, delimiter) - value isa AbstractVector || value isa Tuple || - throw(ArgumentError("parameter style requires an array value")) - return join((_path_scalar(item) for item in value), delimiter) -end - -function _path_object(value, pair_delimiter, key_delimiter) - return join( - ( - string( - _path_scalar(key), - key_delimiter, - _path_scalar(item), - ) for (key, item) in _pairs(value) - ), - pair_delimiter, - ) -end - -function _path_parameter(name, value, style::Symbol, explode::Bool) - encoded = _encode(value) - if encoded === nothing - style === :matrix && return ";" * _path_scalar(name) - style === :label && return "." - style === :simple && return "" - end - if style === :simple - encoded isa AbstractDict && return explode ? - _path_object(encoded, ",", "=") : _path_object(encoded, ",", ",") - encoded isa AbstractVector && return _path_array(encoded, ",") - return _path_scalar(encoded) - elseif style === :label - encoded isa AbstractDict && return "." * (explode ? - _path_object(encoded, ".", "=") : _path_object(encoded, ",", ",")) - encoded isa AbstractVector && return "." * _path_array(encoded, explode ? "." : ",") - return "." * _path_scalar(encoded) - elseif style === :matrix - encoded_name = _path_scalar(name) - if encoded isa AbstractDict - return explode ? - join((";" * _path_scalar(key) * "=" * _path_scalar(item) for (key, item) in _pairs(encoded))) : - ";" * encoded_name * "=" * _path_object(encoded, ",", ",") - elseif encoded isa AbstractVector - return explode ? - join((";" * encoded_name * "=" * _path_scalar(item) for item in encoded)) : - ";" * encoded_name * "=" * _path_array(encoded, ",") - end - return ";" * encoded_name * "=" * _path_scalar(encoded) - end - throw(ArgumentError("unsupported path parameter style $style")) -end - -function _form_component(value, allow_reserved) - encoded = _escape(_scalar(value); allow_reserved) - return replace(encoded, "," => "%2C") -end - -function _query_parameter( - name, - value, - style::Symbol, - explode::Bool, - allow_reserved::Bool, -) - encoded = _encode(value) - if style === :deepObject - if encoded isa AbstractDict - any( - item -> item isa AbstractDict || item isa AbstractVector || item isa Tuple, - values(encoded), - ) && throw(ArgumentError("deepObject does not define nested object or array values")) - return Tuple{String,String,Bool}[ - (string(name, '[', key, ']'), _scalar(item), false) for - (key, item) in _pairs(encoded) - ] - elseif encoded isa AbstractVector || encoded isa Tuple - return Tuple{String,String,Bool}[ - (string(name, "[]"), _scalar(item), false) for item in encoded - ] - end - return [(name, _scalar(encoded), false)] - elseif style in (:spaceDelimited, :pipeDelimited) - explode && throw(ArgumentError("$style with explode=true is undefined")) - delimiter = style === :spaceDelimited ? " " : "|" - if encoded isa AbstractDict - return [(name, _join_object(encoded, delimiter, delimiter), false)] - end - return [(name, _join_array(encoded, delimiter), false)] - elseif style === :form - if encoded isa AbstractDict - return explode ? - Tuple{String,String,Bool}[ - (_scalar(key), _scalar(item), false) for - (key, item) in _pairs(encoded) - ] : - [ - ( - name, - join( - ( - _form_component(key, allow_reserved) * "," * - _form_component(item, allow_reserved) for - (key, item) in _pairs(encoded) - ), - ",", - ), - true, - ), - ] - elseif encoded isa AbstractVector - return explode ? - Tuple{String,String,Bool}[ - (name, _scalar(item), false) for item in encoded - ] : - [ - ( - name, - join( - (_form_component(item, allow_reserved) for item in encoded), - ",", - ), - true, - ), - ] - end - return [(name, _scalar(encoded), false)] - end - throw(ArgumentError("unsupported query parameter style $style")) -end - -function _header_parameter(value, explode::Bool) - encoded = _encode(value) - encoded isa AbstractDict && return explode ? - _join_object(encoded, ",", "=") : _join_object(encoded, ",", ",") - encoded isa AbstractVector && return _join_array(encoded, ",") - return _scalar(encoded) -end - -function _cookie_parameter( - name, - value, - style::Symbol, - explode::Bool, - allow_reserved::Bool, -) - style in (:form, :cookie) || - throw(ArgumentError("unsupported cookie parameter style $style")) - encoded = _encode(value) - if style === :form - pairs = _query_parameter(name, value, :form, explode, allow_reserved) - fragment = join( - ( - _escape(key) * "=" * - (preencoded ? item : _escape(item; allow_reserved)) for - (key, item, preencoded) in pairs - ), - '&', - ) - return [(fragment, "", true, true)] - elseif encoded isa AbstractDict - return explode ? - Tuple{String,String,Bool,Bool}[ - (_scalar(key), _scalar(item), true, false) for - (key, item) in _pairs(encoded) - ] : - [(name, _join_object(encoded, ",", ","), true, false)] - elseif encoded isa AbstractVector - return explode ? - Tuple{String,String,Bool,Bool}[ - (name, _scalar(item), true, false) for item in encoded - ] : - [(name, _join_array(encoded, ","), true, false)] - end - return [(name, _scalar(encoded), true, false)] -end - -_is_hex_digit(char) = isdigit(char) || 'a' <= lowercase(char) <= 'f' - -function _escape(value; allow_reserved::Bool = false) - text = String(value) - allow_reserved || return HTTP.escapeuri(text) - io = IOBuffer() - index = firstindex(text) - while index <= lastindex(text) - char = text[index] - if char == '%' - second = nextind(text, index) - third = second <= lastindex(text) ? nextind(text, second) : second - if third <= lastindex(text) && - _is_hex_digit(text[second]) && - _is_hex_digit(text[third]) - write(io, char, text[second], text[third]) - index = nextind(text, third) - continue - end - end - if char in ":/?#[]@!\$&'()*+,;=" - write(io, char) - else - write(io, HTTP.escapeuri(string(char))) - end - index = nextind(text, index) - end - return String(take!(io)) -end - -function _parameter_content_value(client, entry, value) - encoded = _encoded_media_value(client, value, entry[1]) - encoded isa Upload && (encoded = encoded.data) - if encoded isa AbstractVector{UInt8} - isvalid(String, encoded) && return String(copy(encoded)), false - return join( - ('%' * uppercase(string(byte; base = 16, pad = 2)) for byte in encoded), - ), true - end - return String(encoded), false -end - -function _append_parameter!(client, path, query, headers, cookies, descriptor, value) - location = descriptor.location - style = descriptor.style - explode = descriptor.explode - if location === :querystring - throw(ArgumentError("OAS 3.2 querystring parameter generation is not implemented")) - elseif !isempty(descriptor.content) - serialized, preencoded = - _parameter_content_value(client, first(descriptor.content), value) - if location === :path - path = replace( - path, - "{" * descriptor.name * "}" => - (preencoded ? serialized : _escape(serialized)), - ) - elseif location === :query - push!( - query, - ( - descriptor.name, - serialized, - descriptor.allow_reserved, - preencoded, - ), - ) - elseif location === :header - _set_header!(headers, descriptor.name, serialized) - elseif location === :cookie - push!(cookies, (descriptor.name, serialized, preencoded, false)) - else - throw(ArgumentError("unsupported parameter location $location")) - end - elseif location === :path - serialized = _path_parameter(descriptor.name, value, style, explode) - path = replace(path, "{" * descriptor.name * "}" => serialized) - elseif location === :query - for (name, item, preencoded) in _query_parameter( - descriptor.name, - value, - style, - explode, - descriptor.allow_reserved, - ) - push!(query, (name, item, descriptor.allow_reserved, preencoded)) - end - elseif location === :header - _set_header!(headers, descriptor.name, _header_parameter(value, explode)) - elseif location === :cookie - append!( - cookies, - _cookie_parameter( - descriptor.name, - value, - style, - explode, - descriptor.allow_reserved, - ), - ) - else - throw(ArgumentError("unsupported parameter location $location")) - end - return path -end - -_authorizations(credential::AbstractCredential) = credential.authorizations - -function _credential_satisfies(scheme, credential, required) - valid_type = if scheme.type === :apikey - credential isa ApiKeyCredential - elseif scheme.type === :http_basic - credential isa BasicCredential - elseif scheme.type in (:http_bearer, :oauth2, :openidconnect) - credential isa BearerCredential - elseif scheme.type === :mutualtls - credential isa MutualTLSCredential - elseif scheme.type === :http - credential isa HttpCredential - else - false - end - valid_type || return false - return all(item -> item in _authorizations(credential), required) -end - -function _security!(client, requirements, query, headers, cookies, base_options) - isempty(requirements) && return base_options - for requirement in requirements - isempty(requirement) && return base_options - all(requirement) do entry - credential = get(client.credentials, entry[1], nothing) - credential === nothing && return false - scheme = get(_SECURITY_SCHEMES, entry[1], nothing) - return scheme !== nothing && - _credential_satisfies(scheme, credential, entry[2]) - end || continue - options = base_options - for (name, _) in requirement - scheme = _SECURITY_SCHEMES[name] - credential = client.credentials[name] - if scheme.type === :apikey - credential isa ApiKeyCredential || - throw(ArgumentError("security scheme $name requires ApiKeyCredential")) - if scheme.location === :header - _set_header!(headers, scheme.name, credential.value) - elseif scheme.location === :query - push!(query, (scheme.name, credential.value, false, false)) - elseif scheme.location === :cookie - push!(cookies, (scheme.name, credential.value, false, false)) - end - elseif scheme.type === :http_basic - credential isa BasicCredential || - throw(ArgumentError("security scheme $name requires BasicCredential")) - token = Base64.base64encode(credential.username * ":" * credential.password) - _set_header!(headers, "Authorization", "Basic " * token) - elseif scheme.type in (:http_bearer, :oauth2, :openidconnect) - credential isa BearerCredential || - throw(ArgumentError("security scheme $name requires BearerCredential")) - _set_header!(headers, "Authorization", "Bearer " * credential.token) - elseif scheme.type === :mutualtls - credential isa MutualTLSCredential || - throw(ArgumentError("security scheme $name requires MutualTLSCredential")) - options = merge(options, credential.request_options) - elseif scheme.type === :http - credential isa HttpCredential || - throw(ArgumentError("security scheme $name requires HttpCredential")) - _set_header!( - headers, - "Authorization", - scheme.scheme * " " * credential.value, - ) - else - throw(ArgumentError("security scheme type $(scheme.type) is not supported")) - end - end - return options - end - names = join((join(first.(requirement), " + ") for requirement in requirements), " or ") - client.require_credentials && throw( - ArgumentError("no configured credentials satisfy operation security: " * names), - ) - return base_options -end - -function _decode_response_headers(client, descriptor, headers) - output = Dict{String,Any}() - descriptor === nothing && return output - for header in descriptor.headers - values = _header_values(headers, header.name) - if isempty(values) - header.required && throw( - DecodeError("required response header $(repr(header.name)) is missing"), - ) - continue - end - decoded = if isempty(header.content) - _decode_schema_header( - header.type, - values, - header.shape, - header.explode, - header.schema, - set_cookie = lowercase(header.name) == "set-cookie", - validate = client.validate_responses, - ) - else - entry = first(header.content) - separator = lowercase(header.name) == "set-cookie" ? '\n' : ',' - _decode_body( - client, - header.type, - entry[1], - Vector{UInt8}(codeunits(join(values, separator))), - entry[3], - ) - end - output[header.name] = decoded - end - return output -end - -function _decode_body(client::Client, type, content_type, body, schema) - media = _base_media_type(content_type) - decoder = _select_media_codec(client.media_decoders, content_type) - if decoder !== nothing - value = decoder(copy(body), String(content_type)) - lowered = _encode(value) - client.validate_responses && - _validate_schema( - schema, - lowered, - "decoding a custom-media response"; - direction = :output, - ) - return _decode(type, lowered, client.validate_responses) - elseif _is_json_media(media) - if isempty(body) - client.validate_responses && - _validate_schema( - schema, - nothing, - "decoding an empty JSON response"; - direction = :output, - ) - return _decode(type, nothing, client.validate_responses) - end - value = _parse_json(body, "decoding a response") - client.validate_responses && - _validate_schema( - schema, - value, - "decoding a JSON response"; - direction = :output, - ) - return _decode(type, value, client.validate_responses) - elseif _is_sequential_json_media(media) - value = _decode_sequential_json(body, media) - client.validate_responses && - _validate_schema( - schema, - value, - "decoding a sequential JSON response"; - direction = :output, - ) - return _decode(type, value, client.validate_responses) - elseif startswith(media, "text/") || type === String - isvalid(String, body) || throw(DecodeError("response text is not UTF-8")) - value = String(copy(body)) - client.validate_responses && - _validate_schema( - schema, - value, - "decoding a text response"; - direction = :output, - ) - return _decode(type, value, client.validate_responses) - elseif type === Vector{UInt8} || type === Any || media == "application/octet-stream" - client.validate_responses && _validate_schema( - schema, - Base64.base64encode(body), - "decoding a binary response", - ; direction = :output, - ) - return type === Any ? copy(body) : - _decode(type, body, client.validate_responses) - end - throw(UnsupportedMediaType(String(content_type), :response)) -end - -_form_fields(value::AbstractDict) = Pair{String,Any}[ - String(key) => item for (key, item) in value -] -_form_fields(value::NamedTuple) = Pair{String,Any}[ - String(key) => item for (key, item) in pairs(value) -] -function _form_fields(value) - encoded = _encode(value) - encoded isa AbstractDict || - throw(ArgumentError("form request body must encode as an object")) - return Pair{String,Any}[String(key) => item for (key, item) in encoded] -end - -function _body_bytes(value, media_type) - value isa Upload && return copy(value.data) - value isa AbstractVector{UInt8} && return Vector{UInt8}(value) - value isa AbstractString && return Vector{UInt8}(codeunits(value)) - throw( - ArgumentError( - "codec for $(repr(media_type)) must return a string, byte vector, or Upload", - ), - ) -end - -function _encoded_media_value(client, value, media_type) - media = _base_media_type(media_type) - if _is_json_media(media) - return JSON.json(_encode(value)) - elseif startswith(media, "text/") - return _scalar(value) - end - encoder = _select_media_codec(client.media_encoders, media_type) - encoder === nothing || return encoder(value, String(media_type)) - value isa Upload && return value.data - value isa AbstractVector{UInt8} && return value - value isa AbstractString && return String(value) - throw(UnsupportedMediaType(String(media_type), :request)) -end - -function _configured_content_type(configured, preferred = nothing) - configured === nothing && return preferred - options = String[ - strip(item) for item in split(String(configured), ',') if !isempty(strip(item)) - ] - isempty(options) && throw(ArgumentError("encoding contentType is empty")) - if preferred !== nothing - any(option -> _media_match(String(preferred), option), options) || throw( - ArgumentError( - "part Content-Type $(repr(preferred)) is not allowed by encoding contentType $(repr(configured))", - ), - ) - return String(preferred) - end - return first(options) -end - -function _form_value(client, value, configured_type = nothing) - if configured_type !== nothing - selected_type = _configured_content_type(configured_type) - encoded = _encoded_media_value(client, value, selected_type) - encoded isa Upload && return Base64.base64encode(encoded.data) - encoded isa AbstractVector{UInt8} && return Base64.base64encode(encoded) - return String(encoded) - end - value isa Upload && return Base64.base64encode(value.data) - value isa AbstractVector{UInt8} && return Base64.base64encode(value) - value isa AbstractDict && return JSON.json(_encode(value)) - value isa NamedTuple && return JSON.json(_encode(value)) - encoded = _encode(value) - encoded isa AbstractDict && return JSON.json(encoded) - return _scalar(encoded) -end - -function _form_pairs(client, value, encodings) - output = Tuple{String,String,Bool,Bool}[] - configured = Dict(encoding.name => encoding for encoding in encodings) - for (key, item) in _form_fields(value) - encoding = get(configured, key, nothing) - if encoding !== nothing && encoding.rfc6570 - style = something(encoding.style, :form) - explode = something(encoding.explode, style === :form) - allow_reserved = encoding.allow_reserved - for (name, encoded, preencoded) in _query_parameter( - key, - item, - style, - explode, - allow_reserved, - ) - push!( - output, - (name, encoded, allow_reserved, preencoded), - ) - end - continue - end - configured_type = encoding === nothing ? nothing : encoding.content_type - if item isa AbstractVector && !(item isa AbstractVector{UInt8}) - for child in item - push!( - output, - (String(key), _form_value(client, child, configured_type), false, false), - ) - end - else - push!( - output, - (String(key), _form_value(client, item, configured_type), false, false), - ) - end - end - return output -end - -_form_escape(value) = replace(_escape(value), "%20" => "+") - -function _safe_multipart_text(value, label) - text = String(value) - (occursin('\r', text) || occursin('\n', text)) && - throw(ArgumentError("multipart $label must not contain CR or LF")) - return text -end - -function _multipart_quoted_text(value, label) - text = _safe_multipart_text(value, label) - return replace( - text, - Char(92) => string(Char(92), Char(92)), - Char(34) => string(Char(92), Char(34)), - ) -end - -function _multipart_content( - client, - value, - configured_type, - nested_encodings, - nested_headers, -) - has_nested_headers = !isempty( - _string_keyed_map(nested_headers, "nested multipart_headers"), - ) - if !isempty(nested_encodings) && configured_type === nothing - throw( - ArgumentError( - "nested Encoding Objects require an explicit multipart or form contentType", - ), - ) - end - if value isa Upload || value isa AbstractVector{UInt8} - (isempty(nested_encodings) && !has_nested_headers) || throw( - ArgumentError("binary multipart values cannot contain nested encodings"), - ) - end - if value isa Upload - content_type = something( - _configured_content_type(configured_type, value.content_type), - "application/octet-stream", - ) - return value.data, value.filename, content_type, value.headers - elseif value isa AbstractVector{UInt8} - return Vector{UInt8}(value), nothing, - something( - _configured_content_type(configured_type), - "application/octet-stream", - ), - Pair{String,String}[] - elseif configured_type !== nothing - selected_type = _configured_content_type(configured_type) - if !isempty(nested_encodings) - encoded, actual_type = _encode_body( - client, - value, - selected_type, - nested_encodings; - multipart_headers = nested_headers, - ) - return _body_bytes(encoded, actual_type), nothing, actual_type, - Pair{String,String}[] - end - has_nested_headers && throw( - ArgumentError("nested multipart headers require nested Encoding Objects"), - ) - encoded = _encoded_media_value(client, value, selected_type) - return _body_bytes(encoded, selected_type), nothing, selected_type, - Pair{String,String}[] - elseif value isa AbstractString || value isa Number || value isa Bool - has_nested_headers && throw( - ArgumentError("nested multipart headers require nested Encoding Objects"), - ) - return Vector{UInt8}(codeunits(_scalar(value))), nothing, - something(configured_type, "text/plain"), - Pair{String,String}[] - end - has_nested_headers && throw( - ArgumentError("nested multipart headers require nested Encoding Objects"), - ) - return Vector{UInt8}(codeunits(JSON.json(_encode(value)))), nothing, - something(configured_type, "application/json"), - Pair{String,String}[] -end - -function _multipart_style_pairs(name, value, style, explode) - encoded = _encode(value) - if style === :deepObject - encoded isa AbstractDict || throw(ArgumentError("deepObject requires an object")) - return Pair{String,Any}[ - string(name, '[', key, ']') => item for (key, item) in _pairs(encoded) - ] - elseif style in (:spaceDelimited, :pipeDelimited) - explode && throw(ArgumentError("$style with explode=true is undefined")) - delimiter = style === :spaceDelimited ? " " : "|" - return [String(name) => (encoded isa AbstractDict ? - _join_object(encoded, delimiter, delimiter) : - _join_array(encoded, delimiter))] - elseif style === :form - if encoded isa AbstractDict - return explode ? - Pair{String,Any}[String(key) => item for (key, item) in _pairs(encoded)] : - [String(name) => _join_object(encoded, ",", ",")] - elseif encoded isa AbstractVector - return explode ? [String(name) => item for item in encoded] : - [String(name) => _join_array(encoded, ",")] - end - return [String(name) => encoded] - end - throw(ArgumentError("unsupported multipart parameter style $style")) -end - -function _string_keyed_map(value, label) - value isa AbstractDict || value isa NamedTuple || throw( - ArgumentError("$label must be a dictionary or named tuple"), - ) - output = Dict{String,Any}() - for (key, item) in pairs(value) - name = String(key) - haskey(output, name) && throw( - ArgumentError("$label contains duplicate key $(repr(name))"), - ) - output[name] = item - end - return output -end - -function _multipart_encoding_headers(client, encoding, supplied) - supplied_map = _string_keyed_map(supplied, "multipart part headers") - documented = Dict(lowercase(header.name) => header for header in encoding.headers) - values = Dict{String,Any}() - for (name, value) in supplied_map - lowered = lowercase(name) - haskey(values, lowered) && throw( - ArgumentError( - "multipart part headers contain duplicate case-insensitive name $(repr(name))", - ), - ) - descriptor = get(documented, lowered, nothing) - descriptor === nothing && throw( - ArgumentError( - "multipart header $(repr(name)) is not documented for part $(repr(encoding.name))", - ), - ) - values[lowered] = value - end - - output = Pair{String,String}[] - for descriptor in encoding.headers - lowered = lowercase(descriptor.name) - if !haskey(values, lowered) - descriptor.required && throw( - ArgumentError( - "required multipart header $(repr(descriptor.name)) is missing for part $(repr(encoding.name))", - ), - ) - continue - end - value = values[lowered] - _validate_schema( - descriptor.schema, - _encode(value), - "encoding multipart header $(descriptor.name)"; - direction = :input, - ) - serialized = if isempty(descriptor.content) - _header_parameter(value, descriptor.explode) - else - text, binary = _parameter_content_value( - client, - first(descriptor.content), - value, - ) - binary && throw( - ArgumentError( - "multipart header content for $(repr(descriptor.name)) must encode as text", - ), - ) - text - end - push!(output, _safe_header(descriptor.name, serialized)) - end - return output -end - -function _multipart_part_headers(value) - value isa MultipartPartHeaders && return value.values, value.parts - return value, NamedTuple() -end - -function _merge_multipart_headers(upload_headers, encoding_headers) - output = Pair{String,String}[] - seen = Set{String}() - for (name, value) in Iterators.flatten((upload_headers, encoding_headers)) - lowered = lowercase(String(name)) - lowered in ("content-type", "content-disposition") && throw( - ArgumentError( - "multipart part headers must not override Content-Type or Content-Disposition", - ), - ) - lowered in seen && throw( - ArgumentError( - "multipart part contains duplicate case-insensitive header $(repr(name))", - ), - ) - push!(seen, lowered) - push!(output, _safe_header(name, value)) - end - return output -end - -function _multipart_body( - client, - value, - media_type, - encodings, - multipart_headers, -) - boundary = replace(string(UUIDs.uuid4()), "-" => "") - configured = Dict(encoding.name => encoding for encoding in encodings) - supplied = _string_keyed_map(multipart_headers, "multipart_headers") - used_supplied = Set{String}() - io = IOBuffer() - for (name, raw_item) in _form_fields(value) - encoding = get(configured, name, nothing) - fields = if encoding !== nothing && encoding.rfc6570 - _multipart_style_pairs( - name, - raw_item, - something(encoding.style, :form), - something(encoding.explode, true), - ) - else - items = raw_item isa AbstractVector && - !(raw_item isa AbstractVector{UInt8}) ? raw_item : (raw_item,) - Pair{String,Any}[String(name) => item for item in items] - end - configured_type = encoding === nothing || encoding.rfc6570 ? nothing : - encoding.content_type - supplied_configuration = get(supplied, name, NamedTuple()) - haskey(supplied, name) && push!(used_supplied, name) - supplied_headers, nested_headers = - _multipart_part_headers(supplied_configuration) - declared_headers = if encoding === nothing - isempty(_string_keyed_map(supplied_headers, "multipart part headers")) || throw( - ArgumentError( - "multipart headers were supplied for undocumented part $(repr(name))", - ), - ) - Pair{String,String}[] - else - _multipart_encoding_headers(client, encoding, supplied_headers) - end - nested_encodings = encoding === nothing ? () : encoding.encoding - if encoding === nothing && - !isempty(_string_keyed_map(nested_headers, "nested multipart_headers")) - throw( - ArgumentError( - "nested multipart headers were supplied for undocumented part $(repr(name))", - ), - ) - end - for (part_name, item) in fields - content, filename, content_type, upload_headers = - _multipart_content( - client, - item, - configured_type, - nested_encodings, - nested_headers, - ) - headers = _merge_multipart_headers(upload_headers, declared_headers) - safe_name = _multipart_quoted_text(part_name, "part name") - write(io, "--", boundary, "\r\n") - write(io, "Content-Disposition: form-data; name=", Char(34), safe_name, Char(34)) - if filename !== nothing - safe_filename = _multipart_quoted_text(filename, "filename") - write(io, "; filename=", Char(34), safe_filename, Char(34)) - end - write(io, "\r\nContent-Type: ", _safe_multipart_text(content_type, "content type"), "\r\n") - for (header, header_value) in headers - write( - io, - _safe_multipart_text(header, "header name"), - ": ", - _safe_multipart_text(header_value, "header value"), - "\r\n", - ) - end - write(io, "\r\n", content, "\r\n") - end - end - unused = setdiff(Set(keys(supplied)), used_supplied) - isempty(unused) || throw( - ArgumentError( - "multipart headers were supplied for absent parts: " * - join(repr.(sort!(collect(unused))), ", "), - ), - ) - write(io, "--", boundary, "--\r\n") - return take!(io), String(media_type) * "; boundary=" * boundary -end - - -function _encode_body( - client, - value, - media_type, - encodings; - multipart_headers = NamedTuple(), -) - media = _base_media_type(media_type) - if !startswith(media, "multipart/") - isempty(_string_keyed_map(multipart_headers, "multipart_headers")) || throw( - ArgumentError( - "multipart_headers can only be used with a multipart request body", - ), - ) - end - if _is_json_media(media) - return JSON.json(_encode(value)), media_type - elseif _is_sequential_json_media(media) - return _encode_sequential_json(value, media), media_type - elseif media == "application/x-www-form-urlencoded" - pairs = _form_pairs(client, value, encodings) - body = join( - ( - _form_escape(key) * "=" * - (preencoded ? item : replace( - _escape(item; allow_reserved), - "%20" => "+", - )) for (key, item, allow_reserved, preencoded) in pairs - ), - '&', - ) - return body, media_type - elseif startswith(media, "multipart/") - return _multipart_body( - client, - value, - media_type, - encodings, - multipart_headers, - ) - elseif startswith(media, "text/") - return value isa AbstractString ? String(value) : _scalar(value), media_type - end - return _body_bytes(_encoded_media_value(client, value, media_type), media_type), media_type -end - -function _server_for(client::Client, operation) - if client.server !== nothing - return String(rstrip(client.server, '/')) - end - if client === DEFAULT_CLIENT && SERVER[] != _DEFAULT_SERVER - return String(rstrip(SERVER[], '/')) - end - servers = operation.servers - isempty(servers) && return _DEFAULT_SERVER - selected = if client.server_name === nothing - client.server_index <= length(servers) || throw( - ArgumentError( - "server index $(client.server_index) exceeds the $(length(servers)) documented servers for $(operation.id)", - ), - ) - servers[client.server_index] - else - index = findfirst(server -> server.name == client.server_name, servers) - index === nothing && throw( - ArgumentError( - "server name $(repr(client.server_name)) is not documented for $(operation.id)", - ), - ) - servers[index] - end - url = selected.url - for variable in selected.variables - value = get(client.server_variables, variable.name, variable.default) - isempty(variable.values) || value in variable.values || throw( - ArgumentError( - "server variable $(repr(variable.name)) value $(repr(value)) is not in its documented enum", - ), - ) - url = replace(url, "{" * variable.name * "}" => value) - end - occursin('{', url) && throw( - ArgumentError("not all server URL variables were supplied for $(operation.id)"), - ) - if startswith(lowercase(url), "http://") || startswith(lowercase(url), "https://") - return String(rstrip(url, '/')) - end - base = selected.base - if startswith(lowercase(base), "http://") || startswith(lowercase(base), "https://") - resolved = SchemaEngine.Resources.URIs.resolvereference( - SchemaEngine.Resources.URIs.URI(base), - SchemaEngine.Resources.URIs.URI(url), - ) - return String(rstrip(string(resolved), '/')) - end - return String(rstrip(_DEFAULT_SERVER * (startswith(url, '/') ? url : "/" * url), '/')) -end - -function _request( - client::Client, - operation, - values::Dict{Symbol,Any}; - body = ABSENT, - content_type = nothing, - accept = nothing, - with_http_info::Bool = false, - request_headers = Pair{String,String}[], - request_options::NamedTuple = NamedTuple(), - multipart_headers = NamedTuple(), - stream_to::Union{Nothing,Channel} = nothing, -) - path = operation.path - query = Tuple{String,String,Bool,Bool}[] - headers = Pair{String,String}[_safe_header(key, value) for (key, value) in client.headers] - for (key, value) in request_headers - _set_header!(headers, key, value) - end - cookies = Tuple{String,String,Bool,Bool}[] - for descriptor in operation.parameters - value = get(values, descriptor.arg, ABSENT) - if value isa Absent - descriptor.required && - throw(ArgumentError("required parameter $(descriptor.name) is missing")) - continue - end - client.validate_requests && _validate_schema( - descriptor.schema, - _encode(value), - "encoding parameter $(descriptor.name)"; - direction = :input, - ) - path = _append_parameter!( - client, - path, - query, - headers, - cookies, - descriptor, - value, - ) - end - occursin('{', path) && - throw(ArgumentError("not all path template parameters were supplied for $(operation.id)")) - default_options = stream_to isa Channel ? (; protocol = :h1) : NamedTuple() - base_options = merge(default_options, client.request_options, request_options) - options = _security!( - client, - operation.security, - query, - headers, - cookies, - base_options, - ) - if !isempty(cookies) - cookie_text = join( - ( - fragment ? key : - (raw ? key : _escape(key)) * "=" * - (raw ? value : _escape(value)) for - (key, value, raw, fragment) in cookies - ), - "; ", - ) - existing = join(_header_values(headers, "Cookie"), "; ") - _set_header!( - headers, - "Cookie", - isempty(existing) ? cookie_text : existing * "; " * cookie_text, - ) - options = merge(options, (; cookies = false)) - end - query_text = isempty(query) ? "" : "?" * join( - ( - _escape(key) * "=" * - (preencoded ? value : _escape(value; allow_reserved)) for - (key, value, allow_reserved, preencoded) in query - ), - '&', - ) - url = _server_for(client, operation) * - (startswith(path, '/') ? path : "/" * path) * query_text - - payload = UInt8[] - if operation.request !== nothing && !(body isa Absent) - media = operation.request.media - entry = content_type === nothing ? first(media) : - _select_media(media, String(content_type)) - entry === nothing && throw( - ArgumentError("unsupported request Content-Type $(repr(content_type)) for $(operation.id)"), - ) - client.validate_requests && _validate_schema( - entry[3], - _encode(body), - "encoding the $(operation.id) request body"; - direction = :input, - ) - payload, actual_content_type = - _encode_body( - client, - body, - entry[1], - entry[4]; - multipart_headers, - ) - _set_header!(headers, "Content-Type", actual_content_type) - elseif operation.request !== nothing && operation.request.required - throw(ArgumentError("required request body is missing for $(operation.id)")) - end - documented_accept = String[ - entry[1] for response in operation.responses for entry in response.media - if startswith(uppercase(response.selector), "2") || - uppercase(response.selector) == "DEFAULT" - ] - existing_accept = _header_values(headers, "Accept") - selected_accept = if accept !== nothing - String(accept) - elseif !isempty(existing_accept) - nothing - else - join(unique(documented_accept), ", ") - end - selected_accept === nothing || isempty(selected_accept) || - _set_header!(headers, "Accept", selected_accept) - - stream_to === nothing || return _stream_request( - client, - operation, - url, - headers, - payload, - options, - stream_to, - with_http_info, - ) - options = merge(options, (body = payload, status_exception = false)) - response = HTTP.request( - operation.method, - url, - headers; - options..., - ) - response_headers = Pair{String,String}[ - String(key) => String(value) for (key, value) in response.headers - ] - return _finish_buffered_response( - client, - operation, - Int(response.status), - response_headers, - HTTP.header(response, "Content-Type", ""), - Vector{UInt8}(response.body), - with_http_info, - ) -end - -# Deployed servers routinely omit or misreport Content-Type, so decode by -# status alone unless several documented media types make the choice ambiguous. -# Returns the selected media entry and the media type to decode the body as. -function _selected_media_entry(operation_id, status, descriptor, received) - selected = isempty(received) ? nothing : - _select_media(descriptor.media, received) - selected === nothing || return selected, String(received) - if isempty(received) || length(descriptor.media) == 1 - entry = first(descriptor.media) - return entry, entry[1] - end - throw( - UnexpectedContentType( - operation_id, - status, - received, - Tuple(first.(descriptor.media)), - ), - ) -end - -function _finish_buffered_response( - client::Client, - operation, - status::Int, - response_headers, - received, - bytes::Vector{UInt8}, - with_http_info::Bool, -) - descriptor = _select_response(operation.responses, status) - decoded = nothing - decoded_headers = Dict{String,Any}() - decode_error = nothing - try - decoded_headers = _decode_response_headers(client, descriptor, response_headers) - if descriptor !== nothing - if isempty(descriptor.media) - if !isempty(bytes) && 200 <= status < 300 - throw(UnexpectedBody(operation.id, status, length(bytes))) - end - decoded = nothing - else - selected, actual_media = - _selected_media_entry(operation.id, status, descriptor, received) - decoded = _decode_body( - client, - selected[2], - actual_media, - bytes, - selected[3], - ) - end - else - # The response status is not documented. Specs regularly leave - # data-less statuses (204, redirects) out, so a successful call - # must not fail here: surface an empty body as `nothing` and any - # payload as raw bytes. - decoded = isempty(bytes) ? nothing : copy(bytes) - end - catch error - 200 <= status < 300 && rethrow() - decode_error = error - end - 200 <= status < 300 || throw( - ApiError( - operation.id, - status, - response_headers, - decoded_headers, - bytes, - decoded, - decode_error, - ), - ) - return with_http_info ? - ApiResponse(status, response_headers, decoded_headers, decoded) : - decoded -end - -# ── streaming responses ────────────────────────────────────────────────────── -# -# Passing `stream_to::Channel` to an operation delivers the response body -# incrementally: the call returns as soon as the response head arrives, a -# background task splits the body into items, decodes each one, and `put!`s it -# on the channel. The channel is closed when the response ends, or closed with -# the error when splitting or decoding fails. Closing the channel from the -# consumer side aborts the transfer. - -# How the body is split into items, and what each item decodes to: -# `:json` re-uses the documented response schema per item (concatenated or -# newline-separated JSON documents, e.g. watch-style endpoints); sequential -# JSON media types split records and decode each to the documented array's -# element type; `text/*` yields lines; anything else yields raw byte chunks. -# A registered decoder receives each framed item and its actual Content-Type. -# Its return value is delivered directly, so it can override an inaccurate -# response schema such as a Kubernetes List declaration on a watch stream. -function _stream_plan(media, type, schema) - if _is_sequential_json_media(media) - item_type = type <: AbstractVector && type !== Vector{UInt8} ? - eltype(type) : type - kind = media == "application/json-seq" || endswith(media, "+json-seq") ? - :jsonseq : :jsonlines - # The documented schema describes the whole sequence, not one record, - # so per-item validation is skipped. - return kind, item_type, nothing - elseif _is_json_media(media) - return :json, type, schema - elseif startswith(media, "text/") || type === String - return :text, type, nothing - end - return :bytes, Vector{UInt8}, nothing -end - -_stream_whitespace(byte::UInt8) = - byte == 0x20 || byte == 0x09 || byte == 0x0d || byte == 0x0a - -function _next_stream_line!(buffer::Vector{UInt8}, final::Bool) - while !isempty(buffer) - index = findfirst(==(0x0a), buffer) - if index === nothing - final || return nothing - frame = copy(buffer) - empty!(buffer) - else - frame = buffer[1:index-1] - deleteat!(buffer, 1:index) - end - while !isempty(frame) && frame[end] == 0x0d - pop!(frame) - end - isempty(frame) || return frame - end - return nothing -end - -# Extract the next complete item from `buffer`, or return `nothing` when more -# bytes are needed. `final` marks the end of the response body. -function _next_stream_frame!(buffer::Vector{UInt8}, kind::Symbol, final::Bool) - if kind === :jsonseq - # RFC 7464: records start with RS and may contain unescaped newlines, - # so a record is complete at the next RS or at the end of the body. - while true - start = 1 - while start <= length(buffer) && - (buffer[start] == 0x1e || _stream_whitespace(buffer[start])) - start += 1 - end - start > 1 && deleteat!(buffer, 1:start-1) - isempty(buffer) && return nothing - index = findfirst(==(0x1e), buffer) - if index === nothing - final || return nothing - frame = copy(buffer) - empty!(buffer) - else - frame = buffer[1:index-1] - deleteat!(buffer, 1:index-1) - end - while !isempty(frame) && _stream_whitespace(frame[end]) - pop!(frame) - end - isempty(frame) || return frame - end - end - kind === :json || return _next_stream_line!(buffer, final) - start = 1 - while start <= length(buffer) && - (_stream_whitespace(buffer[start]) || buffer[start] == 0x1e) - start += 1 - end - start > 1 && deleteat!(buffer, 1:start-1) - isempty(buffer) && return nothing - open_byte = buffer[1] - if open_byte == UInt8('{') || open_byte == UInt8('[') - close_byte = open_byte == UInt8('{') ? UInt8('}') : UInt8(']') - depth = 0 - in_string = false - escaped = false - for (index, byte) in enumerate(buffer) - if escaped - escaped = false - elseif in_string - byte == UInt8('\\') && (escaped = true) - byte == UInt8('"') && (in_string = false) - elseif byte == UInt8('"') - in_string = true - elseif byte == open_byte - depth += 1 - elseif byte == close_byte - depth -= 1 - if depth == 0 - frame = buffer[1:index] - deleteat!(buffer, 1:index) - return frame - end - end - end - return nothing - end - # top-level scalar items (numbers, strings, booleans) end at a newline - return _next_stream_line!(buffer, final) -end - -function _decode_stream_item( - client::Client, - frame::Vector{UInt8}, - kind::Symbol, - item_type, - schema, - media_type, -) - decoder = _select_media_codec(client.media_decoders, media_type) - decoder === nothing || return decoder(copy(frame), String(media_type)) - if kind === :text - isvalid(String, frame) || - throw(DecodeError("streaming text response is not UTF-8")) - return _decode(item_type, String(frame), client.validate_responses) - end - kind === :bytes && return copy(frame) - value = _parse_json(frame, "decoding a streaming response item") - schema === nothing || !client.validate_responses || _validate_schema( - schema, - value, - "decoding a streaming response item"; - direction = :output, - ) - return _decode(item_type, value, client.validate_responses) -end - -function _drain_stream_buffer!( - client::Client, - channel::Channel, - buffer::Vector{UInt8}, - kind::Symbol, - item_type, - schema, - media_type, - final::Bool, -) - while true - frame = _next_stream_frame!(buffer, kind, final) - frame === nothing && break - put!( - channel, - _decode_stream_item(client, frame, kind, item_type, schema, media_type), - ) - end - final && kind === :json && !all(_stream_whitespace, buffer) && - throw(DecodeError("streaming response ended with a truncated item")) - return nothing -end +# Emitted at the top of every generated client module. The protocol machinery +# lives in OpenAPI.Runtime; generated modules import the names their models and +# operations use, extending only _decode/_encode/_form_fields with methods on +# their own (module-local) model types. +const GENERATED_CLIENT_IMPORTS = raw""" +const Runtime = OpenAPI.Runtime +const SchemaEngine = OpenAPI.SchemaEngine +import OpenAPI.Runtime: + ABSENT, Absent, AbstractCredential, ApiError, ApiKeyCredential, ApiResponse, + BasicCredential, BearerCredential, DecodeError, HttpCredential, + MultipartPartHeaders, MutualTLSCredential, SchemaValidationError, + UnexpectedBody, UnexpectedContentType, UnsupportedMediaType, Upload, + _decode, _encode, _form_fields, _object, _required, _request, + _schema_valid, _validate_schema +""" -function _pump_stream!( - client::Client, - stream, - channel::Channel, - kind::Symbol, - item_type, - schema, - media_type, - finished, -) - buffer = UInt8[] - try - while !eof(stream) - chunk = readavailable(stream) - isempty(chunk) && continue - if kind === :bytes - put!( - channel, - _decode_stream_item( - client, - chunk, - kind, - item_type, - schema, - media_type, - ), - ) - else - append!(buffer, chunk) - _drain_stream_buffer!( - client, - channel, - buffer, - kind, - item_type, - schema, - media_type, - false, - ) - end - end - kind === :bytes || _drain_stream_buffer!( - client, - channel, - buffer, - kind, - item_type, - schema, - media_type, - true, - ) - finished[] = true - close(channel) - catch error - # A channel the consumer closed is the abort signal; anything else is - # delivered to the consumer through the channel. - if isopen(channel) - finished[] = true - close(channel, error) - end - finally - try - HTTP.closeread(stream) - catch - end - finished[] = true - end - return nothing -end +# Emitted after the generated module's `_SPEC` constant. These forwarders keep +# the public surface of a generated module (Client(), DEFAULT_CLIENT, +# server!, credential!, ...) while delegating to OpenAPI.Runtime. They are +# deliberately module-local functions, not methods added to Runtime generics, +# so independently generated modules never overwrite each other. +const GENERATED_CLIENT_GLUE = raw""" +const SERVER = _SPEC.server -function _abort_stream_on_close!(stream, channel::Channel, producer::Task, finished) - while isopen(channel) && !finished[] - sleep(0.25) - end - finished[] && return nothing - try - HTTP.closeread(stream) - catch - end - for _ in 1:4 - istaskdone(producer) && return nothing - sleep(0.25) - end - try - close(stream) - catch - end - return nothing -end +Client(server::Union{Nothing,AbstractString} = nothing; kwargs...) = + Runtime.Client(_SPEC, server; kwargs...) +const DEFAULT_CLIENT = Runtime.Client(_SPEC; _default = true) -function _stream_request( - client::Client, - operation, - url, - headers, - payload::Vector{UInt8}, - options, - stream_to::Channel, - with_http_info::Bool, -) - stream = HTTP.open(operation.method, url, headers; options...) - local response - try - isempty(payload) || write(stream, payload) - HTTP.closewrite(stream) - response = HTTP.startread(stream) - catch - try - HTTP.closeread(stream) - catch - end - rethrow() - end - response_headers = Pair{String,String}[ - String(key) => String(value) for (key, value) in response.headers - ] - received = HTTP.header(response, "Content-Type", "") - status = Int(response.status) - if !(200 <= status < 300) - bytes = try - read(stream) - finally - try - HTTP.closeread(stream) - catch - end - end - # Throws ApiError with the fully decoded error body. - return _finish_buffered_response( - client, - operation, - status, - response_headers, - received, - bytes, - with_http_info, - ) - end - local descriptor, decoded_headers, kind, item_type, schema, actual_media - try - descriptor = _select_response(operation.responses, status) - decoded_headers = _decode_response_headers(client, descriptor, response_headers) - if descriptor === nothing || isempty(descriptor.media) - actual_media = isempty(received) ? "application/octet-stream" : String(received) - kind, item_type, schema = :bytes, Vector{UInt8}, nothing - else - selected, actual_media = - _selected_media_entry(operation.id, status, descriptor, received) - kind, item_type, schema = - _stream_plan(_base_media_type(actual_media), selected[2], selected[3]) - end - catch - try - HTTP.closeread(stream) - catch - end - rethrow() - end - finished = Ref(false) - producer = errormonitor(@async _pump_stream!( - client, - stream, - stream_to, - kind, - item_type, - schema, - actual_media, - finished, - )) - errormonitor(@async _abort_stream_on_close!(stream, stream_to, producer, finished)) - return with_http_info ? - ApiResponse(status, response_headers, decoded_headers, stream_to) : - stream_to -end +credential!(client::Runtime.Client, name::AbstractString, credential::AbstractCredential) = + Runtime.credential!(client, name, credential) +credential!(name::AbstractString, credential::AbstractCredential) = + Runtime.credential!(DEFAULT_CLIENT, name, credential) +clearcredential!(client::Runtime.Client, name::AbstractString) = + Runtime.clearcredential!(client, name) +clearcredential!(name::AbstractString) = Runtime.clearcredential!(DEFAULT_CLIENT, name) +server!(client::Runtime.Client, server::Union{Nothing,AbstractString}) = + Runtime.server!(client, server) +server!(server::Union{Nothing,AbstractString}) = Runtime.server!(DEFAULT_CLIENT, server) +server_index!(client::Runtime.Client, index::Integer) = Runtime.server_index!(client, index) +server_index!(index::Integer) = Runtime.server_index!(DEFAULT_CLIENT, index) +server_name!(client::Runtime.Client, name::Union{Nothing,AbstractString}) = + Runtime.server_name!(client, name) +server_name!(name::Union{Nothing,AbstractString}) = Runtime.server_name!(DEFAULT_CLIENT, name) +server_variable!(client::Runtime.Client, name::AbstractString, value::AbstractString) = + Runtime.server_variable!(client, name, value) +server_variable!(name::AbstractString, value::AbstractString) = + Runtime.server_variable!(DEFAULT_CLIENT, name, value) +codec!(client::Runtime.Client, media_type::AbstractString; kwargs...) = + Runtime.codec!(client, media_type; kwargs...) +codec!(media_type::AbstractString; kwargs...) = + Runtime.codec!(DEFAULT_CLIENT, media_type; kwargs...) +authorization!(client::Runtime.Client, token::Union{Nothing,AbstractString}) = + Runtime.authorization!(client, token) +authorization!(token::Union{Nothing,AbstractString}) = + Runtime.authorization!(DEFAULT_CLIENT, token) """ function _julia_literal(value) @@ -2805,12 +282,12 @@ function _emit_model( ) println(io, "_decode(::Type{", model.name, "}, value) = _decode(", model.name, ", value, true)") println(io, "function _decode(::Type{", model.name, "}, value, _openapi_validate::Bool)") - println(io, " _openapi_validate && _validate_schema(", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") + println(io, " _openapi_validate && _validate_schema(_SPEC, ", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") println(io, " return ", model.name, "(_decode(", type, ", value, _openapi_validate))") println(io, "end") println(io, "function _encode(value::", model.name, ")") println(io, " output = _encode(value.value)") - println(io, " return _validate_schema(", schema, ", output, ", repr("encoding " * model.name), "; direction = ", direction, ")") + println(io, " return _validate_schema(_SPEC, ", schema, ", output, ", repr("encoding " * model.name), "; direction = ", direction, ")") println(io, "end\n") return elseif model.kind === :alias @@ -2843,12 +320,12 @@ function _emit_model( ) println(io, "_decode(::Type{", model.name, "}, value) = _decode(", model.name, ", value, true)") println(io, "function _decode(::Type{", model.name, "}, value, _openapi_validate::Bool)") - println(io, " _openapi_validate && _validate_schema(", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") + println(io, " _openapi_validate && _validate_schema(_SPEC, ", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") println(io, " return ", model.name, "(_decode(", model.alias, ", value, _openapi_validate))") println(io, "end") println(io, "function _encode(value::", model.name, ")") println(io, " output = _encode(value.value)") - println(io, " return _validate_schema(", schema, ", output, ", repr("encoding " * model.name), "; direction = ", direction, ")") + println(io, " return _validate_schema(_SPEC, ", schema, ", output, ", repr("encoding " * model.name), "; direction = ", direction, ")") println(io, "end") println(io, "Base.string(value::", model.name, ") = string(value.value)\n") return @@ -2878,7 +355,7 @@ function _emit_model( (!isempty(model.discriminator_mapping) || model.discriminator_default !== nothing) println(io, "_decode(::Type{", model.name, "}, value) = _decode(", model.name, ", value, true)") println(io, "function _decode(::Type{", model.name, "}, value, _openapi_validate::Bool)") - println(io, " _openapi_validate && _validate_schema(", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") + println(io, " _openapi_validate && _validate_schema(_SPEC, ", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") println(io, " object = _object(value, ", repr(model.name), ")") println(io, " tag = get(object, ", repr(model.discriminator), ", ABSENT)") println(io, " tag isa Absent || tag isa AbstractString || throw(DecodeError(\"discriminator value must be a string for ", model.name, "\"))") @@ -2909,13 +386,13 @@ function _emit_model( ) end println(io, " selected === nothing && throw(DecodeError(\"unknown discriminator value \$(repr(tag)) for ", model.name, "\"))") - println(io, " !_openapi_validate || _schema_valid(selected[2], value; direction = ", direction, ") || throw(DecodeError(\"discriminator-selected schema did not validate for ", model.name, "\"))") + println(io, " !_openapi_validate || _schema_valid(_SPEC, selected[2], value; direction = ", direction, ") || throw(DecodeError(\"discriminator-selected schema did not validate for ", model.name, "\"))") println(io, " return ", model.name, "(_decode(selected[1], value, _openapi_validate))") println(io, "end") else println(io, "_decode(::Type{", model.name, "}, value) = _decode(", model.name, ", value, true)") println(io, "function _decode(::Type{", model.name, "}, value, _openapi_validate::Bool)") - println(io, " _openapi_validate && _validate_schema(", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") + println(io, " _openapi_validate && _validate_schema(_SPEC, ", schema, ", value, ", repr("decoding " * model.name), "; direction = ", direction, ")") "Nothing" in model.values && println( io, " value === nothing && return ", @@ -2925,7 +402,7 @@ function _emit_model( println(io, " matches = Any[]") for (node, type) in model.variants descriptor = _schema_descriptor(node) - println(io, " if !_openapi_validate || _schema_valid(", descriptor, ", value; direction = ", direction, ")") + println(io, " if !_openapi_validate || _schema_valid(_SPEC, ", descriptor, ", value; direction = ", direction, ")") println(io, " try") println(io, " push!(matches, _decode(", type, ", value, _openapi_validate))") println(io, " catch error") @@ -2943,7 +420,7 @@ function _emit_model( end println(io, "function _encode(value::", model.name, ")") println(io, " output = _encode(value.value)") - println(io, " return _validate_schema(", schema, ", output, ", repr("encoding " * model.name), "; direction = ", direction, ")") + println(io, " return _validate_schema(_SPEC, ", schema, ", output, ", repr("encoding " * model.name), "; direction = ", direction, ")") println(io, "end\n") return end @@ -2991,7 +468,7 @@ function _emit_model( ) println(io, "_decode(::Type{", model.name, "}, value) = _decode(", model.name, ", value, true)") println(io, "function _decode(::Type{", model.name, "}, _openapi_raw, _openapi_validate::Bool)") - println(io, " _openapi_validate && _validate_schema(", schema, ", _openapi_raw, ", repr("decoding " * model.name), "; direction = ", direction, ")") + println(io, " _openapi_validate && _validate_schema(_SPEC, ", schema, ", _openapi_raw, ", repr("decoding " * model.name), "; direction = ", direction, ")") println(io, " _openapi_object = _object(_openapi_raw, ", repr(model.name), ")") for field in model.fields type = _rewrite_forward(field.type, index, indices, abstract_targets) @@ -3054,7 +531,7 @@ function _emit_model( println(io, " _openapi_output[_openapi_key] = _encode(_openapi_item)") println(io, " end") end - println(io, " return _validate_schema(", schema, ", _openapi_output, ", repr("encoding " * model.name), "; direction = ", direction, ")") + println(io, " return _validate_schema(_SPEC, ", schema, ", _openapi_output, ", repr("encoding " * model.name), "; direction = ", direction, ")") println(io, "end\n") println(io, "function _form_fields(_openapi_value::", model.name, ")") println(io, " _openapi_output = Pair{String,Any}[]") @@ -3662,7 +1139,7 @@ function _emit_operation(io, operation::OperationPlan, const_name) append!( keywords, [ - "client::Client = DEFAULT_CLIENT", + "client::Runtime.Client = DEFAULT_CLIENT", "content_type::Union{Nothing,AbstractString} = nothing", "accept::Union{Nothing,AbstractString} = nothing", "with_http_info::Bool = false", @@ -3718,16 +1195,18 @@ function _generate(plan::ClientPlan) println(io, "module ", plan.module_name, "\n") println(io, "using HTTP, JSON, OpenAPI, Base64, Dates, UUIDs") plan.datetime === :zoned && println(io, "using TimeZones") - println(io, "const SchemaEngine = OpenAPI.SchemaEngine\n") + print(io, GENERATED_CLIENT_IMPORTS, '\n') _emit_security(io, plan) _emit_schema_data(io, plan) - default_server = _default_server(plan.api) - print(io, GENERATED_RUNTIME_COMMON, '\n') - plan.datetime === :zoned && print(io, GENERATED_ZONED_RUNTIME, '\n') - print(io, GENERATED_RUNTIME, '\n') - println(io, "const _DEFAULT_SERVER = ", repr(default_server)) - println(io, "const SERVER = Ref{String}(_DEFAULT_SERVER)") - println(io, "const DEFAULT_CLIENT = Client()\n") + println(io, "const _SPEC = Runtime.Spec(;") + println(io, " security_schemes = _SECURITY_SCHEMES,") + println(io, " resources = _SCHEMA_RESOURCE_DATA,") + println(io, " roots = _SCHEMA_ROOT_DATA,") + println(io, " dialects = _SCHEMA_DIALECT_DATA,") + println(io, " directional_required = _SCHEMA_DIRECTIONAL_REQUIRED,") + println(io, " default_server = ", repr(_default_server(plan.api)), ",") + println(io, ")\n") + print(io, GENERATED_CLIENT_GLUE, '\n') indices = _model_indices(plan) wrapped_aliases = _cyclic_aliases(plan) diff --git a/src/runtime.jl b/src/runtime.jl index 43a6d55..267a3bb 100644 --- a/src/runtime.jl +++ b/src/runtime.jl @@ -1,9 +1,2280 @@ -"""Runtime support shared by generated clients.""" +""" +Runtime support for generated OpenAPI clients. + +Generated client modules import this module's machinery instead of carrying a +pasted copy: protocol encoding and decoding, parameter styling, content +negotiation, multipart bodies, security schemes, and the HTTP request core. +Each generated module packages its document-specific data — compiled schema +resources, security schemes, and the default server — into a [`Spec`](@ref) +that its `Client` values carry. +""" module Runtime +import ..SchemaEngine +using JSON, Base64, Dates, UUIDs + +""" +Document-specific data a generated module supplies to the shared runtime: +schema resources for validation, security schemes, and server defaults. +Mutable runtime state (the module-wide server override and the compiled +schema-graph cache) lives here so independent generated modules never share +or clobber each other's state. +""" +struct Spec + security_schemes::Dict{String,NamedTuple} + resources::Vector{Any} + roots::Vector{Any} + dialects::Vector{Any} + directional_required::Vector{Any} + default_server::String + server::Base.RefValue{String} + graphs::Dict{Symbol,Any} + graph_lock::ReentrantLock +end + +function Spec(; + security_schemes::Dict{String,NamedTuple} = Dict{String,NamedTuple}(), + resources::Vector{Any} = Any[], + roots::Vector{Any} = Any[], + dialects::Vector{Any} = Any[], + directional_required::Vector{Any} = Any[], + default_server::AbstractString = "", +) + return Spec( + security_schemes, + resources, + roots, + dialects, + directional_required, + String(default_server), + Ref(String(default_server)), + Dict{Symbol,Any}(), + ReentrantLock(), + ) +end + struct Absent end const ABSENT = Absent() +Base.show(io::IO, ::Absent) = print(io, "ABSENT") + +struct DecodeError <: Exception + message::String +end +Base.showerror(io::IO, error::DecodeError) = print(io, error.message) + +struct UnsupportedMediaType <: Exception + media_type::String + direction::Symbol +end +Base.showerror(io::IO, error::UnsupportedMediaType) = print( + io, + "no ", + error.direction, + " codec is configured for media type ", + repr(error.media_type), +) + +struct SchemaValidationError <: Exception + context::String + issues::Vector{Any} +end +function Base.showerror(io::IO, error::SchemaValidationError) + print(io, "schema validation failed while ", error.context) + for issue in error.issues + print(io, "\n ", issue.path, ": ", issue.reason) + end +end + +function _schema_graph(spec::Spec, direction::Symbol = :neutral) + direction in (:neutral, :input, :output) || + throw(ArgumentError("schema direction must be :neutral, :input, or :output")) + isempty(spec.roots) && return nothing + haskey(spec.graphs, direction) && return spec.graphs[direction] + return lock(spec.graph_lock) do + haskey(spec.graphs, direction) && return spec.graphs[direction] + documents = Dict{String,Any}( + entry.id => JSON.parse(entry.json; duplicate_keys = :error) for + entry in spec.resources + ) + if direction !== :neutral + for rule in spec.directional_required + removed = direction === :input ? rule.input : rule.output + isempty(removed) && continue + document = get(documents, rule.resource, nothing) + document === nothing && continue + schema = SchemaEngine.Resources.resolve( + document, + SchemaEngine.Resources.JSONPointer(rule.pointer), + ) + schema isa AbstractDict || continue + required = get(schema, "required", nothing) + required isa AbstractVector || continue + retained = Any[ + name for name in required if String(name) ∉ removed + ] + isempty(retained) ? delete!(schema, "required") : + (schema["required"] = retained) + end + end + resources = SchemaEngine.Resources.Resource[] + for entry in spec.resources + id = SchemaEngine.Resources.ResourceId(entry.id) + retrieval = SchemaEngine.Resources.ResourceId(entry.retrieval) + push!( + resources, + SchemaEngine.Resources.Resource( + id, + documents[entry.id]; + retrieval, + media_type = entry.media_type, + ), + ) + end + roots = SchemaEngine.Resources.NodeId[ + SchemaEngine.Resources.NodeId( + SchemaEngine.Resources.ResourceId(entry.resource), + SchemaEngine.Resources.JSONPointer(entry.pointer), + ) for entry in spec.roots + ] + root_dialects = Dict( + root => entry.dialect for (root, entry) in zip(roots, spec.roots) + ) + dialect_aliases = Dict( + entry.uri => SchemaEngine.Dialect( + entry.name, + entry.uri, + entry.id_keyword, + entry.ref_siblings, + entry.modern_items, + entry.unevaluated, + entry.dynamic_refs, + entry.recursive_refs, + entry.applicator, + entry.validation, + ) for entry in spec.dialects + ) + graph = SchemaEngine.CompiledSchemas( + resources, + roots; + dialect = SchemaEngine.DRAFT202012, + root_dialects, + dialect_aliases, + ) + spec.graphs[direction] = graph + return graph + end +end + +function _schema_at(spec::Spec, descriptor, direction::Symbol = :neutral) + descriptor === nothing && return nothing + graph = _schema_graph(spec, direction) + graph === nothing && return nothing + node = SchemaEngine.Resources.NodeId( + SchemaEngine.Resources.ResourceId(descriptor.resource), + SchemaEngine.Resources.JSONPointer(descriptor.pointer), + ) + return SchemaEngine.subschema(graph, node) +end + +function _schema_issues(spec::Spec, descriptor, value; direction::Symbol = :neutral) + schema = _schema_at(spec, descriptor, direction) + schema === nothing && return Any[] + return Any[SchemaEngine.validate(schema, value; fail_fast = false)...] +end + +function _validate_schema( + spec::Spec, + descriptor, + value, + context; + direction::Symbol = :neutral, +) + issues = _schema_issues(spec, descriptor, value; direction) + isempty(issues) || throw(SchemaValidationError(String(context), issues)) + return value +end + +_schema_valid(spec::Spec, descriptor, value; direction::Symbol = :neutral) = + isempty(_schema_issues(spec, descriptor, value; direction)) + +struct Upload + data::Vector{UInt8} + filename::Union{Nothing,String} + content_type::Union{Nothing,String} + headers::Vector{Pair{String,String}} +end + +# Header values for one multipart part and, when needed, its nested parts. +struct MultipartPartHeaders + values::Any + parts::Any +end +MultipartPartHeaders(values = NamedTuple(); parts = NamedTuple()) = + MultipartPartHeaders(values, parts) +function Upload( + data::AbstractVector{UInt8}; + filename::Union{Nothing,AbstractString} = nothing, + content_type::Union{Nothing,AbstractString} = nothing, + headers = Pair{String,String}[], +) + return Upload( + Vector{UInt8}(data), + filename === nothing ? nothing : String(filename), + content_type === nothing ? nothing : String(content_type), + Pair{String,String}[String(key) => String(value) for (key, value) in headers], + ) +end + +_typename(::Type{T}) where {T} = string(T) +_required(value, key, model) = haskey(value, key) ? value[key] : + throw(DecodeError("required field $(repr(key)) is missing while decoding $model")) +_object(value, model) = value isa AbstractDict ? value : + throw(DecodeError("expected an object while decoding $model, got $(typeof(value))")) + +_decode(::Type{Any}, value) = value +_decode(::Type{Nothing}, ::Nothing) = nothing +_decode(::Type{Nothing}, value) = throw(DecodeError("expected null, got $(typeof(value))")) +_decode(::Type{String}, value::AbstractString) = String(value) +_decode(::Type{Bool}, value::Bool) = value +_decode(::Type{T}, value::Integer) where {T<:Integer} = try + value isa Bool && throw(DecodeError("expected an integer, got Bool")) + convert(T, value) +catch + throw(DecodeError("integer $value does not fit $T")) +end +function _decode(::Type{T}, value::Real) where {T<:AbstractFloat} + value isa Bool && throw(DecodeError("expected a number, got Bool")) + return convert(T, value) +end +function _decode(::Type{Dates.Date}, value::AbstractString) + try + return Dates.Date(value) + catch error + throw(DecodeError("invalid RFC 3339 full-date: $(sprint(showerror, error))")) + end +end +function _decode(::Type{Dates.Time}, value::AbstractString) + try + return Dates.Time(value) + catch error + throw(DecodeError("invalid time: $(sprint(showerror, error))")) + end +end +function _decode(::Type{Dates.DateTime}, value::AbstractString) + # RFC 3339 requires the offset, but zone-less ISO 8601 date-times are + # widespread in deployed APIs (most JSON serializers print naive + # timestamps). Accept input liberally: a missing offset means UTC, the + # same convention _encode uses when it stamps naive DateTimes with `Z`. + matched = match( + r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})?$", + value, + ) + matched === nothing && throw(DecodeError("invalid RFC 3339 date-time $(repr(value))")) + try + output = Dates.DateTime(matched.captures[1]) + fraction = matched.captures[2] + if fraction !== nothing + milliseconds = parse(Int, rpad(first(fraction, min(3, length(fraction))), 3, '0')) + output += Dates.Millisecond(milliseconds) + end + zone = matched.captures[3] + if zone !== nothing && zone != "Z" + sign = startswith(zone, '+') ? 1 : -1 + hours = parse(Int, zone[2:3]) + minutes = parse(Int, zone[5:6]) + output -= Dates.Minute(sign * (60 * hours + minutes)) + end + return output + catch error + error isa DecodeError && rethrow() + throw(DecodeError("invalid RFC 3339 date-time: $(sprint(showerror, error))")) + end +end +function _decode(::Type{UUIDs.UUID}, value::AbstractString) + try + return UUIDs.UUID(value) + catch error + throw(DecodeError("invalid UUID: $(sprint(showerror, error))")) + end +end +function _decode(::Type{Vector{UInt8}}, value::AbstractString) + try + return Base64.base64decode(value) + catch error + throw(DecodeError("invalid base64 data: $(sprint(showerror, error))")) + end +end +_decode(::Type{Vector{UInt8}}, value::AbstractVector{UInt8}) = copy(value) + +_decode(::Type{T}, value::AbstractVector) where {T<:AbstractVector} = + _decode(T, value, true) +function _decode(::Type{T}, value::AbstractVector, validate::Bool) where {T<:AbstractVector} + element = eltype(T) + return T([_decode(element, item, validate) for item in value]) +end +_decode(::Type{T}, value::AbstractDict) where {T<:AbstractDict} = + _decode(T, value, true) +function _decode(::Type{T}, value::AbstractDict, validate::Bool) where {T<:AbstractDict} + keytype(T) <: AbstractString || + throw(DecodeError("only string-key dictionaries are supported, got $T")) + output = T() + for (key, item) in value + output[convert(keytype(T), key)] = _decode(valtype(T), item, validate) + end + return output +end +_decode(::Type{T}, value::AbstractVector) where {T<:Tuple} = + _decode(T, value, true) +function _decode(::Type{T}, value::AbstractVector, validate::Bool) where {T<:Tuple} + length(value) == fieldcount(T) || + throw(DecodeError("expected $(fieldcount(T)) tuple items, got $(length(value))")) + return T(( + _decode(fieldtype(T, index), value[index], validate) for + index in 1:fieldcount(T) + )...) +end + +function _direct_union_match(::Type{T}, value) where {T} + T === Nothing && return value === nothing + T === Bool && return value isa Bool + T <: Integer && return value isa Integer && !(value isa Bool) + T <: AbstractFloat && return value isa AbstractFloat + T <: AbstractString && return value isa AbstractString + T <: AbstractVector && return value isa AbstractVector + T <: AbstractDict && return value isa AbstractDict + return value isa T +end + +function _decode_union( + ::Type{T}, + value; + oneof::Bool = false, + validate::Bool = true, +) where {T} + variants = Base.uniontypes(T) + if value === nothing && Nothing in variants + return nothing + end + if !oneof + preferred = Any[ + variant for variant in variants + if variant ∉ (Absent, Nothing) && _direct_union_match(variant, value) + ] + append!(preferred, Any[variant for variant in variants if variant ∉ preferred]) + variants = preferred + end + successes = Any[] + failures = String[] + for variant in variants + variant in (Absent, Nothing) && continue + try + decoded = _decode(variant, value, validate) + oneof || return decoded + push!(successes, decoded) + catch error + error isa DecodeError || rethrow() + push!(failures, error.message) + end + end + length(successes) == 1 && return only(successes) + isempty(successes) && throw( + DecodeError("value does not match any variant of $T: " * join(failures, "; ")), + ) + throw(DecodeError("value matches more than one variant of $T")) +end + +function _decode(::Type{T}, value) where {T} + T isa Union && return _decode_union(T, value) + value isa T && return value + try + return convert(T, value) + catch + throw(DecodeError("cannot decode $(typeof(value)) as $T")) + end +end + +function _decode(::Type{T}, value, validate::Bool) where {T} + T isa Union && return _decode_union(T, value; validate) + return _decode(T, value) +end + +_encode(::Absent) = throw(ArgumentError("ABSENT is only valid as an object field")) +_encode(::Nothing) = nothing +_encode(value::Union{Bool,Number,AbstractString}) = value +_encode(value::Union{Dates.Date,Dates.Time,UUIDs.UUID}) = string(value) +_encode(value::Dates.DateTime) = Dates.format(value, dateformat"yyyy-mm-ddTHH:MM:SS.sss") * "Z" +_encode(value::AbstractVector{UInt8}) = Base64.base64encode(value) +_encode(value::Upload) = Base64.base64encode(value.data) +_encode(value::AbstractVector) = Any[_encode(item) for item in value] +_encode(value::Tuple) = Any[_encode(item) for item in value] +function _encode(value::AbstractDict) + output = JSON.Object{String,Any}() + for (key, item) in value + output[String(key)] = _encode(item) + end + return output +end +function _encode(value::NamedTuple) + output = JSON.Object{String,Any}() + for (key, item) in pairs(value) + output[String(key)] = _encode(item) + end + return output +end +_encode(value) = value + +function _safe_header(name, value) + header = String(name) + content = String(value) + occursin(r"^[!#$%&'*+.^_`|~0-9A-Za-z-]+$", header) || + throw(ArgumentError("invalid HTTP header name $(repr(header))")) + (occursin('\r', content) || occursin('\n', content)) && + throw(ArgumentError("HTTP header values must not contain CR or LF")) + return header => content +end + +function _set_header!(headers, name, value) + pair = _safe_header(name, value) + lowered = lowercase(pair.first) + filter!(entry -> lowercase(entry.first) != lowered, headers) + push!(headers, pair) + return headers +end + +function _media_segments(value) + output = String[] + io = IOBuffer() + quoted = false + escaped = false + for char in String(value) + if escaped + write(io, char) + escaped = false + elseif quoted && char == '\\' + write(io, char) + escaped = true + elseif char == '"' + write(io, char) + quoted = !quoted + elseif char == ';' && !quoted + push!(output, String(take!(io))) + else + write(io, char) + end + end + push!(output, String(take!(io))) + return output +end + +function _media_parameter_value(value) + text = strip(String(value)) + startswith(text, '"') && endswith(text, '"') && ncodeunits(text) >= 2 || + return text + ncodeunits(text) == 2 && return "" + io = IOBuffer() + escaped = false + for char in SubString(text, nextind(text, firstindex(text)), prevind(text, lastindex(text))) + if escaped + write(io, char) + escaped = false + elseif char == '\\' + escaped = true + else + write(io, char) + end + end + escaped && write(io, '\\') + return String(take!(io)) +end + +function _media_type(value) + segments = _media_segments(value) + base = lowercase(strip(first(segments))) + parameters = Dict{String,String}() + for segment in Iterators.drop(segments, 1) + pair = split(segment, '='; limit = 2) + length(pair) == 2 || continue + name = lowercase(strip(pair[1])) + isempty(name) && continue + parameter = _media_parameter_value(pair[2]) + parameters[name] = name == "charset" ? lowercase(parameter) : parameter + end + return base, parameters +end + +function _media_match_score(received::String, documented::String) + received_base = first(_media_type(received)) + documented_base = first(_media_type(documented)) + received_base == documented_base && return 4 + documented_base == "*/*" && return 1 + parts = split(documented_base, '/'; limit = 2) + received_parts = split(received_base, '/'; limit = 2) + length(parts) == 2 && length(received_parts) == 2 || return 0 + parts[1] == received_parts[1] || parts[1] == "*" || return 0 + parts[2] == "*" && return parts[1] == "*" ? 1 : 2 + startswith(parts[2], "*+") && + endswith(received_parts[2], parts[2][2:end]) && return 3 + return 0 +end + +function _media_selection_score(received::String, documented::String) + received_base, received_parameters = _media_type(received) + documented_base, documented_parameters = _media_type(documented) + base_score = _media_match_score(received_base, documented_base) + base_score > 0 || return (0, 0, 0) + all(documented_parameters) do (name, value) + return get(received_parameters, name, nothing) == value + end || return (0, 0, 0) + exact = length(received_parameters) == length(documented_parameters) + return (base_score, length(documented_parameters), exact ? 1 : 0) +end + +_media_match(received::String, documented::String) = + first(_media_selection_score(received, documented)) > 0 + +_base_media_type(value) = lowercase(strip(first(split(String(value), ';'; limit = 2)))) + +function _select_media(media, received) + isempty(media) && return nothing + selected = nothing + score = (0, 0, 0) + for entry in media + candidate = _media_selection_score(String(received), String(entry[1])) + if candidate > score + selected = entry + score = candidate + end + end + return selected +end + +function _select_media_codec(codecs, received) + selected = nothing + score = (0, 0, 0) + for (documented, codec) in codecs + candidate = _media_selection_score(String(received), documented) + if candidate > score + selected = codec + score = candidate + end + end + return selected +end + +function _store_media_codec!(codecs, media_type, codec) + key = strip(String(media_type)) + parsed = _media_type(key) + for existing in collect(keys(codecs)) + _media_type(existing) == parsed && delete!(codecs, existing) + end + codecs[key] = codec + return codecs +end + +function _select_response(responses, status) + exact = string(status) + range = string(div(status, 100), "XX") + default = nothing + for response in responses + selector = uppercase(response.selector) + selector == exact && return response + selector == range && (default = response) + selector == "DEFAULT" && default === nothing && (default = response) + end + return default +end + +function _header_values(headers, name) + lowered = lowercase(String(name)) + return String[ + value for (key, value) in headers if lowercase(String(key)) == lowered + ] +end + +function _header_atom(value) + text = String(value) + lowered = lowercase(text) + lowered == "true" && return true + lowered == "false" && return false + lowered == "null" && return nothing + integer = tryparse(Int64, text) + integer === nothing || return integer + number = tryparse(Float64, text) + number === nothing || return number + return text +end + +function _header_scalar(type, value) + text = String(value) + type === String && return text + type === Bool && return lowercase(text) == "true" ? true : + lowercase(text) == "false" ? false : + throw(DecodeError("invalid boolean value $(repr(text))")) + type <: Integer && return try + parse(type, text) + catch error + throw(DecodeError("invalid integer value: $(sprint(showerror, error))")) + end + type <: AbstractFloat && return try + parse(type, text) + catch error + throw(DecodeError("invalid numeric value: $(sprint(showerror, error))")) + end + return _decode(type, text) +end + +function _header_type_variant(type, shape) + variants = type isa Union ? Base.uniontypes(type) : (type,) + for variant in variants + variant in (Nothing, Absent) && continue + shape === :array && variant <: AbstractVector && return variant + shape === :object && + (variant <: AbstractDict || isstructtype(variant)) && return variant + shape === :scalar && return variant + end + return type +end + +function _decode_schema_header( + spec::Spec, + type, + values, + shape, + explode, + schema; + set_cookie::Bool = false, + direction::Symbol = :output, + context = "decoding a response header", + validate::Bool = true, +) + selected = _header_type_variant(type, shape) + raw = if shape === :array + items = if set_cookie + String[String(value) for value in values] + else + output = String[] + for value in values + append!(output, split(value, ',')) + end + output + end + element = selected <: AbstractVector ? eltype(selected) : String + Any[_header_scalar(element, item) for item in items] + elseif shape === :object + object = JSON.Object{String,Any}() + if set_cookie && explode + for value in values + pair = split(value, '='; limit = 2) + length(pair) == 2 || throw( + DecodeError("invalid Set-Cookie response header"), + ) + object[pair[1]] = pair[2] + end + elseif explode + tokens = split(join(values, ','), ',') + for token in tokens + pair = split(token, '='; limit = 2) + length(pair) == 2 || throw( + DecodeError("invalid exploded object response header"), + ) + object[pair[1]] = _header_atom(pair[2]) + end + else + tokens = split(join(values, ','), ',') + iseven(length(tokens)) || + throw(DecodeError("invalid object response header")) + for index in 1:2:length(tokens) + object[tokens[index]] = _header_atom(tokens[index + 1]) + end + end + object + else + _header_scalar(selected, join(values, set_cookie ? '\n' : ',')) + end + validate && _validate_schema( + spec, + schema, + _encode(raw), + context; + direction, + ) + return _decode(type, raw, validate) +end + +_is_json_media(media) = media == "application/json" || endswith(media, "+json") +_is_sequential_json_media(media) = media in ( + "application/jsonl", + "application/x-ndjson", + "application/json-seq", + "application/geo+json-seq", +) || endswith(media, "+json-seq") + +function _parse_json(text, context) + source = text isa AbstractString ? String(text) : String(copy(text)) + isvalid(source) || throw(DecodeError("invalid UTF-8 while $context")) + try + return JSON.parse(source; duplicate_keys = :error) + catch error + throw(DecodeError("invalid JSON while $context: $(sprint(showerror, error))")) + end +end + +function _decode_sequential_json(body, media) + records = Any[] + if media == "application/json-seq" || endswith(media, "+json-seq") + for chunk in split(String(body), Char(0x1e)) + text = strip(chunk) + isempty(text) || push!(records, _parse_json(text, "decoding a JSON sequence")) + end + else + for line in eachline(IOBuffer(body)) + text = strip(line) + isempty(text) || push!(records, _parse_json(text, "decoding JSON lines")) + end + end + return records +end + +function _encode_sequential_json(value, media) + encoded = _encode(value) + encoded isa AbstractVector || encoded isa Tuple || throw( + ArgumentError("sequential JSON request bodies must be arrays or tuples"), + ) + if media == "application/json-seq" || endswith(media, "+json-seq") + return join((string(Char(0x1e), JSON.json(item), '\n') for item in encoded)) + end + return join((JSON.json(item) * "\n" for item in encoded)) +end +struct ApiError <: Exception + operation_id::String + status::Int + headers::Vector{Pair{String,String}} + decoded_headers::Dict{String,Any} + body::Vector{UInt8} + decoded::Any + decode_error::Any +end +function Base.showerror(io::IO, error::ApiError) + print(io, "ApiError(", error.status, ") in ", error.operation_id) + if error.decode_error !== nothing + print(io, " (response decoding failed: ") + showerror(io, error.decode_error) + print(io, ')') + end + isempty(error.body) && return + text = isvalid(String, error.body) ? String(copy(error.body)) : nothing + if text === nothing + preview = bytes2hex(error.body[1:min(length(error.body), 32)]) + print(io, ": ", length(error.body), " binary bytes (", preview) + length(error.body) > 32 && print(io, "…") + print(io, ')') + else + preview = first(text, min(length(text), 512)) + print(io, ": ", preview) + length(text) > 512 && print(io, "…") + end +end + +struct ApiResponse{T} + status::Int + headers::Vector{Pair{String,String}} + decoded_headers::Dict{String,Any} + body::T +end + +struct UnexpectedBody <: Exception + operation_id::String + status::Int + bytes::Int +end +Base.showerror(io::IO, error::UnexpectedBody) = print( + io, + "operation ", + error.operation_id, + " received an undocumented ", + error.bytes, + "-byte response body for status ", + error.status, +) + +struct UnexpectedContentType <: Exception + operation_id::String + status::Int + received::String + expected::Tuple +end +function Base.showerror(io::IO, error::UnexpectedContentType) + print( + io, + "unexpected Content-Type ", + repr(error.received), + " for ", + error.operation_id, + " response ", + error.status, + "; expected ", + join(error.expected, ", "), + ) +end + +abstract type AbstractCredential end +struct ApiKeyCredential <: AbstractCredential + value::String + authorizations::Set{String} +end +ApiKeyCredential(value::AbstractString; roles = String[]) = + ApiKeyCredential(String(value), Set(String.(roles))) +struct BasicCredential <: AbstractCredential + username::String + password::String + authorizations::Set{String} +end +BasicCredential(username::AbstractString, password::AbstractString; roles = String[]) = + BasicCredential(String(username), String(password), Set(String.(roles))) +struct BearerCredential <: AbstractCredential + token::String + authorizations::Set{String} +end +function BearerCredential(token::AbstractString; scopes = String[], roles = String[]) + return BearerCredential( + String(token), + union(Set(String.(scopes)), Set(String.(roles))), + ) +end +struct HttpCredential <: AbstractCredential + value::String + authorizations::Set{String} +end +HttpCredential(value::AbstractString; roles = String[]) = + HttpCredential(String(value), Set(String.(roles))) +struct MutualTLSCredential <: AbstractCredential + request_options::NamedTuple + authorizations::Set{String} +end +MutualTLSCredential(request_options::NamedTuple; roles = String[]) = + MutualTLSCredential(request_options, Set(String.(roles))) + +mutable struct Client + spec::Spec + is_default::Bool + server::Union{Nothing,String} + server_index::Int + server_name::Union{Nothing,String} + server_variables::Dict{String,String} + credentials::Dict{String,AbstractCredential} + headers::Vector{Pair{String,String}} + request_options::NamedTuple + require_credentials::Bool + media_encoders::Dict{String,Function} + media_decoders::Dict{String,Function} + validate_requests::Bool + validate_responses::Bool +end + +function _normalize_media_codecs(codecs::AbstractDict) + normalized = Dict{String,Function}() + for (name, codec) in codecs + _store_media_codec!(normalized, name, codec) + end + return normalized +end + +function Client( + spec::Spec, + server::Union{Nothing,AbstractString} = nothing; + _default::Bool = false, + server_index::Integer = 1, + server_name::Union{Nothing,AbstractString} = nothing, + server_variables::AbstractDict = Dict{String,String}(), + credentials::AbstractDict = Dict{String,AbstractCredential}(), + headers = Pair{String,String}[], + request_options::NamedTuple = NamedTuple(), + require_credentials::Bool = true, + media_encoders::AbstractDict = Dict{String,Function}(), + media_decoders::AbstractDict = Dict{String,Function}(), + validate_requests::Bool = true, + validate_responses::Bool = true, +) + server_index > 0 || throw(ArgumentError("server_index must be positive")) + normalized_credentials = credentials isa Dict{String,AbstractCredential} ? + copy(credentials) : + Dict{String,AbstractCredential}( + String(name) => credential for (name, credential) in credentials + ) + normalized_headers = headers isa Vector{Pair{String,String}} ? copy(headers) : + Pair{String,String}[ + String(name) => String(value) for (name, value) in headers + ] + return Client( + spec, + _default, + server === nothing ? nothing : String(rstrip(server, '/')), + Int(server_index), + server_name === nothing ? nothing : String(server_name), + server_variables isa Dict{String,String} ? copy(server_variables) : + Dict{String,String}( + String(name) => String(value) for (name, value) in server_variables + ), + normalized_credentials, + normalized_headers, + request_options, + require_credentials, + _normalize_media_codecs(media_encoders), + _normalize_media_codecs(media_decoders), + validate_requests, + validate_responses, + ) +end + +function credential!(client::Client, name::AbstractString, credential::AbstractCredential) + client.credentials[String(name)] = credential + return client +end + +function clearcredential!(client::Client, name::AbstractString) + delete!(client.credentials, String(name)) + return client +end + +function server!(client::Client, server::Union{Nothing,AbstractString}) + client.server = server === nothing ? nothing : String(rstrip(server, '/')) + client.is_default && + (client.spec.server[] = something(client.server, client.spec.default_server)) + return client +end + +function server_index!(client::Client, index::Integer) + index > 0 || throw(ArgumentError("server index must be positive")) + client.server_index = Int(index) + client.server_name = nothing + return client +end + +function server_name!(client::Client, name::Union{Nothing,AbstractString}) + client.server_name = name === nothing ? nothing : String(name) + return client +end + +function server_variable!(client::Client, name::AbstractString, value::AbstractString) + client.server_variables[String(name)] = String(value) + return client +end + +function codec!( + client::Client, + media_type::AbstractString; + encode::Union{Nothing,Function} = nothing, + decode::Union{Nothing,Function} = nothing, +) + encode === nothing || + _store_media_codec!(client.media_encoders, media_type, encode) + decode === nothing || + _store_media_codec!(client.media_decoders, media_type, decode) + return client +end + +function authorization!(client::Client, token::Union{Nothing,AbstractString}) + names = String[ + name for (name, scheme) in client.spec.security_schemes + if scheme.type in (:http_bearer, :oauth2, :openidconnect) + ] + isempty(names) && throw(ArgumentError("the OpenAPI document has no bearer-compatible security scheme")) + for name in names + if token === nothing + delete!(client.credentials, name) + else + client.credentials[name] = BearerCredential(String(token)) + end + end + return client +end + +_scalar(::Nothing) = "" +_scalar(value::Bool) = value ? "true" : "false" +_scalar(value::Dates.DateTime) = _encode(value) +_scalar(value::Union{Dates.Date,Dates.Time,UUIDs.UUID}) = string(value) +_scalar(value::AbstractVector{UInt8}) = Base64.base64encode(value) +_scalar(value) = string(value) + +function _pairs(value) + lowered = _encode(value) + lowered isa AbstractDict || throw(ArgumentError("parameter style requires an object value")) + return Pair{String,Any}[String(key) => item for (key, item) in lowered] +end + +function _join_array(value, delimiter) + value isa AbstractVector || value isa Tuple || + throw(ArgumentError("parameter style requires an array value")) + return join((_scalar(item) for item in value), delimiter) +end + +function _join_object(value, pair_delimiter, key_delimiter) + return join( + (string(_scalar(key), key_delimiter, _scalar(item)) for (key, item) in _pairs(value)), + pair_delimiter, + ) +end + +_path_scalar(value) = _escape(_scalar(value)) + +function _path_array(value, delimiter) + value isa AbstractVector || value isa Tuple || + throw(ArgumentError("parameter style requires an array value")) + return join((_path_scalar(item) for item in value), delimiter) +end + +function _path_object(value, pair_delimiter, key_delimiter) + return join( + ( + string( + _path_scalar(key), + key_delimiter, + _path_scalar(item), + ) for (key, item) in _pairs(value) + ), + pair_delimiter, + ) +end + +function _path_parameter(name, value, style::Symbol, explode::Bool) + encoded = _encode(value) + if encoded === nothing + style === :matrix && return ";" * _path_scalar(name) + style === :label && return "." + style === :simple && return "" + end + if style === :simple + encoded isa AbstractDict && return explode ? + _path_object(encoded, ",", "=") : _path_object(encoded, ",", ",") + encoded isa AbstractVector && return _path_array(encoded, ",") + return _path_scalar(encoded) + elseif style === :label + encoded isa AbstractDict && return "." * (explode ? + _path_object(encoded, ".", "=") : _path_object(encoded, ",", ",")) + encoded isa AbstractVector && return "." * _path_array(encoded, explode ? "." : ",") + return "." * _path_scalar(encoded) + elseif style === :matrix + encoded_name = _path_scalar(name) + if encoded isa AbstractDict + return explode ? + join((";" * _path_scalar(key) * "=" * _path_scalar(item) for (key, item) in _pairs(encoded))) : + ";" * encoded_name * "=" * _path_object(encoded, ",", ",") + elseif encoded isa AbstractVector + return explode ? + join((";" * encoded_name * "=" * _path_scalar(item) for item in encoded)) : + ";" * encoded_name * "=" * _path_array(encoded, ",") + end + return ";" * encoded_name * "=" * _path_scalar(encoded) + end + throw(ArgumentError("unsupported path parameter style $style")) +end + +function _form_component(value, allow_reserved) + encoded = _escape(_scalar(value); allow_reserved) + return replace(encoded, "," => "%2C") +end + +function _query_parameter( + name, + value, + style::Symbol, + explode::Bool, + allow_reserved::Bool, +) + encoded = _encode(value) + if style === :deepObject + if encoded isa AbstractDict + any( + item -> item isa AbstractDict || item isa AbstractVector || item isa Tuple, + values(encoded), + ) && throw(ArgumentError("deepObject does not define nested object or array values")) + return Tuple{String,String,Bool}[ + (string(name, '[', key, ']'), _scalar(item), false) for + (key, item) in _pairs(encoded) + ] + elseif encoded isa AbstractVector || encoded isa Tuple + return Tuple{String,String,Bool}[ + (string(name, "[]"), _scalar(item), false) for item in encoded + ] + end + return [(name, _scalar(encoded), false)] + elseif style in (:spaceDelimited, :pipeDelimited) + explode && throw(ArgumentError("$style with explode=true is undefined")) + delimiter = style === :spaceDelimited ? " " : "|" + if encoded isa AbstractDict + return [(name, _join_object(encoded, delimiter, delimiter), false)] + end + return [(name, _join_array(encoded, delimiter), false)] + elseif style === :form + if encoded isa AbstractDict + return explode ? + Tuple{String,String,Bool}[ + (_scalar(key), _scalar(item), false) for + (key, item) in _pairs(encoded) + ] : + [ + ( + name, + join( + ( + _form_component(key, allow_reserved) * "," * + _form_component(item, allow_reserved) for + (key, item) in _pairs(encoded) + ), + ",", + ), + true, + ), + ] + elseif encoded isa AbstractVector + return explode ? + Tuple{String,String,Bool}[ + (name, _scalar(item), false) for item in encoded + ] : + [ + ( + name, + join( + (_form_component(item, allow_reserved) for item in encoded), + ",", + ), + true, + ), + ] + end + return [(name, _scalar(encoded), false)] + end + throw(ArgumentError("unsupported query parameter style $style")) +end + +function _header_parameter(value, explode::Bool) + encoded = _encode(value) + encoded isa AbstractDict && return explode ? + _join_object(encoded, ",", "=") : _join_object(encoded, ",", ",") + encoded isa AbstractVector && return _join_array(encoded, ",") + return _scalar(encoded) +end + +function _cookie_parameter( + name, + value, + style::Symbol, + explode::Bool, + allow_reserved::Bool, +) + style in (:form, :cookie) || + throw(ArgumentError("unsupported cookie parameter style $style")) + encoded = _encode(value) + if style === :form + pairs = _query_parameter(name, value, :form, explode, allow_reserved) + fragment = join( + ( + _escape(key) * "=" * + (preencoded ? item : _escape(item; allow_reserved)) for + (key, item, preencoded) in pairs + ), + '&', + ) + return [(fragment, "", true, true)] + elseif encoded isa AbstractDict + return explode ? + Tuple{String,String,Bool,Bool}[ + (_scalar(key), _scalar(item), true, false) for + (key, item) in _pairs(encoded) + ] : + [(name, _join_object(encoded, ",", ","), true, false)] + elseif encoded isa AbstractVector + return explode ? + Tuple{String,String,Bool,Bool}[ + (name, _scalar(item), true, false) for item in encoded + ] : + [(name, _join_array(encoded, ","), true, false)] + end + return [(name, _scalar(encoded), true, false)] +end + +# RFC 3986 percent-encoding of everything outside the unreserved set. Local +# so the parameter machinery works without HTTP.jl loaded. +function _escapeuri(text) + io = IOBuffer() + for byte in codeunits(String(text)) + char = Char(byte) + if char in 'A':'Z' || char in 'a':'z' || char in '0':'9' || char in "-_.~" + write(io, char) + else + print(io, '%', uppercase(string(byte; base = 16, pad = 2))) + end + end + return String(take!(io)) +end + +# The HTTP transport core lives in OpenAPIHTTPExt; generated clients say +# `using HTTP`, which loads the extension and adds methods to these stubs. +function _request end +function _stream_request end +function _pump_stream! end +function _abort_stream_on_close! end + +_is_hex_digit(char) = isdigit(char) || 'a' <= lowercase(char) <= 'f' + +function _escape(value; allow_reserved::Bool = false) + text = String(value) + allow_reserved || return _escapeuri(text) + io = IOBuffer() + index = firstindex(text) + while index <= lastindex(text) + char = text[index] + if char == '%' + second = nextind(text, index) + third = second <= lastindex(text) ? nextind(text, second) : second + if third <= lastindex(text) && + _is_hex_digit(text[second]) && + _is_hex_digit(text[third]) + write(io, char, text[second], text[third]) + index = nextind(text, third) + continue + end + end + if char in ":/?#[]@!\$&'()*+,;=" + write(io, char) + else + write(io, _escapeuri(string(char))) + end + index = nextind(text, index) + end + return String(take!(io)) +end + +function _parameter_content_value(client, entry, value) + encoded = _encoded_media_value(client, value, entry[1]) + encoded isa Upload && (encoded = encoded.data) + if encoded isa AbstractVector{UInt8} + isvalid(String, encoded) && return String(copy(encoded)), false + return join( + ('%' * uppercase(string(byte; base = 16, pad = 2)) for byte in encoded), + ), true + end + return String(encoded), false +end -Base.show(io::IO, ::Absent) = print(io, "OpenAPI.Runtime.ABSENT") +function _append_parameter!(client, path, query, headers, cookies, descriptor, value) + location = descriptor.location + style = descriptor.style + explode = descriptor.explode + if location === :querystring + throw(ArgumentError("OAS 3.2 querystring parameter generation is not implemented")) + elseif !isempty(descriptor.content) + serialized, preencoded = + _parameter_content_value(client, first(descriptor.content), value) + if location === :path + path = replace( + path, + "{" * descriptor.name * "}" => + (preencoded ? serialized : _escape(serialized)), + ) + elseif location === :query + push!( + query, + ( + descriptor.name, + serialized, + descriptor.allow_reserved, + preencoded, + ), + ) + elseif location === :header + _set_header!(headers, descriptor.name, serialized) + elseif location === :cookie + push!(cookies, (descriptor.name, serialized, preencoded, false)) + else + throw(ArgumentError("unsupported parameter location $location")) + end + elseif location === :path + serialized = _path_parameter(descriptor.name, value, style, explode) + path = replace(path, "{" * descriptor.name * "}" => serialized) + elseif location === :query + for (name, item, preencoded) in _query_parameter( + descriptor.name, + value, + style, + explode, + descriptor.allow_reserved, + ) + push!(query, (name, item, descriptor.allow_reserved, preencoded)) + end + elseif location === :header + _set_header!(headers, descriptor.name, _header_parameter(value, explode)) + elseif location === :cookie + append!( + cookies, + _cookie_parameter( + descriptor.name, + value, + style, + explode, + descriptor.allow_reserved, + ), + ) + else + throw(ArgumentError("unsupported parameter location $location")) + end + return path +end + +_authorizations(credential::AbstractCredential) = credential.authorizations + +function _credential_satisfies(scheme, credential, required) + valid_type = if scheme.type === :apikey + credential isa ApiKeyCredential + elseif scheme.type === :http_basic + credential isa BasicCredential + elseif scheme.type in (:http_bearer, :oauth2, :openidconnect) + credential isa BearerCredential + elseif scheme.type === :mutualtls + credential isa MutualTLSCredential + elseif scheme.type === :http + credential isa HttpCredential + else + false + end + valid_type || return false + return all(item -> item in _authorizations(credential), required) +end +function _security!(client, requirements, query, headers, cookies, base_options) + isempty(requirements) && return base_options + for requirement in requirements + isempty(requirement) && return base_options + all(requirement) do entry + credential = get(client.credentials, entry[1], nothing) + credential === nothing && return false + scheme = get(client.spec.security_schemes, entry[1], nothing) + return scheme !== nothing && + _credential_satisfies(scheme, credential, entry[2]) + end || continue + options = base_options + for (name, _) in requirement + scheme = client.spec.security_schemes[name] + credential = client.credentials[name] + if scheme.type === :apikey + credential isa ApiKeyCredential || + throw(ArgumentError("security scheme $name requires ApiKeyCredential")) + if scheme.location === :header + _set_header!(headers, scheme.name, credential.value) + elseif scheme.location === :query + push!(query, (scheme.name, credential.value, false, false)) + elseif scheme.location === :cookie + push!(cookies, (scheme.name, credential.value, false, false)) + end + elseif scheme.type === :http_basic + credential isa BasicCredential || + throw(ArgumentError("security scheme $name requires BasicCredential")) + token = Base64.base64encode(credential.username * ":" * credential.password) + _set_header!(headers, "Authorization", "Basic " * token) + elseif scheme.type in (:http_bearer, :oauth2, :openidconnect) + credential isa BearerCredential || + throw(ArgumentError("security scheme $name requires BearerCredential")) + _set_header!(headers, "Authorization", "Bearer " * credential.token) + elseif scheme.type === :mutualtls + credential isa MutualTLSCredential || + throw(ArgumentError("security scheme $name requires MutualTLSCredential")) + options = merge(options, credential.request_options) + elseif scheme.type === :http + credential isa HttpCredential || + throw(ArgumentError("security scheme $name requires HttpCredential")) + _set_header!( + headers, + "Authorization", + scheme.scheme * " " * credential.value, + ) + else + throw(ArgumentError("security scheme type $(scheme.type) is not supported")) + end + end + return options + end + names = join((join(first.(requirement), " + ") for requirement in requirements), " or ") + client.require_credentials && throw( + ArgumentError("no configured credentials satisfy operation security: " * names), + ) + return base_options end + +function _decode_response_headers(client, descriptor, headers) + output = Dict{String,Any}() + descriptor === nothing && return output + for header in descriptor.headers + values = _header_values(headers, header.name) + if isempty(values) + header.required && throw( + DecodeError("required response header $(repr(header.name)) is missing"), + ) + continue + end + decoded = if isempty(header.content) + _decode_schema_header(client.spec, + header.type, + values, + header.shape, + header.explode, + header.schema, + set_cookie = lowercase(header.name) == "set-cookie", + validate = client.validate_responses, + ) + else + entry = first(header.content) + separator = lowercase(header.name) == "set-cookie" ? '\n' : ',' + _decode_body( + client, + header.type, + entry[1], + Vector{UInt8}(codeunits(join(values, separator))), + entry[3], + ) + end + output[header.name] = decoded + end + return output +end + +function _decode_body(client::Client, type, content_type, body, schema) + media = _base_media_type(content_type) + decoder = _select_media_codec(client.media_decoders, content_type) + if decoder !== nothing + value = decoder(copy(body), String(content_type)) + lowered = _encode(value) + client.validate_responses && + _validate_schema(client.spec, + schema, + lowered, + "decoding a custom-media response"; + direction = :output, + ) + return _decode(type, lowered, client.validate_responses) + elseif _is_json_media(media) + if isempty(body) + client.validate_responses && + _validate_schema(client.spec, + schema, + nothing, + "decoding an empty JSON response"; + direction = :output, + ) + return _decode(type, nothing, client.validate_responses) + end + value = _parse_json(body, "decoding a response") + client.validate_responses && + _validate_schema(client.spec, + schema, + value, + "decoding a JSON response"; + direction = :output, + ) + return _decode(type, value, client.validate_responses) + elseif _is_sequential_json_media(media) + value = _decode_sequential_json(body, media) + client.validate_responses && + _validate_schema(client.spec, + schema, + value, + "decoding a sequential JSON response"; + direction = :output, + ) + return _decode(type, value, client.validate_responses) + elseif startswith(media, "text/") || type === String + isvalid(String, body) || throw(DecodeError("response text is not UTF-8")) + value = String(copy(body)) + client.validate_responses && + _validate_schema(client.spec, + schema, + value, + "decoding a text response"; + direction = :output, + ) + return _decode(type, value, client.validate_responses) + elseif type === Vector{UInt8} || type === Any || media == "application/octet-stream" + client.validate_responses && _validate_schema(client.spec, + schema, + Base64.base64encode(body), + "decoding a binary response", + ; direction = :output, + ) + return type === Any ? copy(body) : + _decode(type, body, client.validate_responses) + end + throw(UnsupportedMediaType(String(content_type), :response)) +end + +_form_fields(value::AbstractDict) = Pair{String,Any}[ + String(key) => item for (key, item) in value +] +_form_fields(value::NamedTuple) = Pair{String,Any}[ + String(key) => item for (key, item) in pairs(value) +] +function _form_fields(value) + encoded = _encode(value) + encoded isa AbstractDict || + throw(ArgumentError("form request body must encode as an object")) + return Pair{String,Any}[String(key) => item for (key, item) in encoded] +end + +function _body_bytes(value, media_type) + value isa Upload && return copy(value.data) + value isa AbstractVector{UInt8} && return Vector{UInt8}(value) + value isa AbstractString && return Vector{UInt8}(codeunits(value)) + throw( + ArgumentError( + "codec for $(repr(media_type)) must return a string, byte vector, or Upload", + ), + ) +end + +function _encoded_media_value(client, value, media_type) + media = _base_media_type(media_type) + if _is_json_media(media) + return JSON.json(_encode(value)) + elseif startswith(media, "text/") + return _scalar(value) + end + encoder = _select_media_codec(client.media_encoders, media_type) + encoder === nothing || return encoder(value, String(media_type)) + value isa Upload && return value.data + value isa AbstractVector{UInt8} && return value + value isa AbstractString && return String(value) + throw(UnsupportedMediaType(String(media_type), :request)) +end + +function _configured_content_type(configured, preferred = nothing) + configured === nothing && return preferred + options = String[ + strip(item) for item in split(String(configured), ',') if !isempty(strip(item)) + ] + isempty(options) && throw(ArgumentError("encoding contentType is empty")) + if preferred !== nothing + any(option -> _media_match(String(preferred), option), options) || throw( + ArgumentError( + "part Content-Type $(repr(preferred)) is not allowed by encoding contentType $(repr(configured))", + ), + ) + return String(preferred) + end + return first(options) +end + +function _form_value(client, value, configured_type = nothing) + if configured_type !== nothing + selected_type = _configured_content_type(configured_type) + encoded = _encoded_media_value(client, value, selected_type) + encoded isa Upload && return Base64.base64encode(encoded.data) + encoded isa AbstractVector{UInt8} && return Base64.base64encode(encoded) + return String(encoded) + end + value isa Upload && return Base64.base64encode(value.data) + value isa AbstractVector{UInt8} && return Base64.base64encode(value) + value isa AbstractDict && return JSON.json(_encode(value)) + value isa NamedTuple && return JSON.json(_encode(value)) + encoded = _encode(value) + encoded isa AbstractDict && return JSON.json(encoded) + return _scalar(encoded) +end + +function _form_pairs(client, value, encodings) + output = Tuple{String,String,Bool,Bool}[] + configured = Dict(encoding.name => encoding for encoding in encodings) + for (key, item) in _form_fields(value) + encoding = get(configured, key, nothing) + if encoding !== nothing && encoding.rfc6570 + style = something(encoding.style, :form) + explode = something(encoding.explode, style === :form) + allow_reserved = encoding.allow_reserved + for (name, encoded, preencoded) in _query_parameter( + key, + item, + style, + explode, + allow_reserved, + ) + push!( + output, + (name, encoded, allow_reserved, preencoded), + ) + end + continue + end + configured_type = encoding === nothing ? nothing : encoding.content_type + if item isa AbstractVector && !(item isa AbstractVector{UInt8}) + for child in item + push!( + output, + (String(key), _form_value(client, child, configured_type), false, false), + ) + end + else + push!( + output, + (String(key), _form_value(client, item, configured_type), false, false), + ) + end + end + return output +end + +_form_escape(value) = replace(_escape(value), "%20" => "+") + +function _safe_multipart_text(value, label) + text = String(value) + (occursin('\r', text) || occursin('\n', text)) && + throw(ArgumentError("multipart $label must not contain CR or LF")) + return text +end + +function _multipart_quoted_text(value, label) + text = _safe_multipart_text(value, label) + return replace( + text, + Char(92) => string(Char(92), Char(92)), + Char(34) => string(Char(92), Char(34)), + ) +end + +function _multipart_content( + client, + value, + configured_type, + nested_encodings, + nested_headers, +) + has_nested_headers = !isempty( + _string_keyed_map(nested_headers, "nested multipart_headers"), + ) + if !isempty(nested_encodings) && configured_type === nothing + throw( + ArgumentError( + "nested Encoding Objects require an explicit multipart or form contentType", + ), + ) + end + if value isa Upload || value isa AbstractVector{UInt8} + (isempty(nested_encodings) && !has_nested_headers) || throw( + ArgumentError("binary multipart values cannot contain nested encodings"), + ) + end + if value isa Upload + content_type = something( + _configured_content_type(configured_type, value.content_type), + "application/octet-stream", + ) + return value.data, value.filename, content_type, value.headers + elseif value isa AbstractVector{UInt8} + return Vector{UInt8}(value), nothing, + something( + _configured_content_type(configured_type), + "application/octet-stream", + ), + Pair{String,String}[] + elseif configured_type !== nothing + selected_type = _configured_content_type(configured_type) + if !isempty(nested_encodings) + encoded, actual_type = _encode_body( + client, + value, + selected_type, + nested_encodings; + multipart_headers = nested_headers, + ) + return _body_bytes(encoded, actual_type), nothing, actual_type, + Pair{String,String}[] + end + has_nested_headers && throw( + ArgumentError("nested multipart headers require nested Encoding Objects"), + ) + encoded = _encoded_media_value(client, value, selected_type) + return _body_bytes(encoded, selected_type), nothing, selected_type, + Pair{String,String}[] + elseif value isa AbstractString || value isa Number || value isa Bool + has_nested_headers && throw( + ArgumentError("nested multipart headers require nested Encoding Objects"), + ) + return Vector{UInt8}(codeunits(_scalar(value))), nothing, + something(configured_type, "text/plain"), + Pair{String,String}[] + end + has_nested_headers && throw( + ArgumentError("nested multipart headers require nested Encoding Objects"), + ) + return Vector{UInt8}(codeunits(JSON.json(_encode(value)))), nothing, + something(configured_type, "application/json"), + Pair{String,String}[] +end + +function _multipart_style_pairs(name, value, style, explode) + encoded = _encode(value) + if style === :deepObject + encoded isa AbstractDict || throw(ArgumentError("deepObject requires an object")) + return Pair{String,Any}[ + string(name, '[', key, ']') => item for (key, item) in _pairs(encoded) + ] + elseif style in (:spaceDelimited, :pipeDelimited) + explode && throw(ArgumentError("$style with explode=true is undefined")) + delimiter = style === :spaceDelimited ? " " : "|" + return [String(name) => (encoded isa AbstractDict ? + _join_object(encoded, delimiter, delimiter) : + _join_array(encoded, delimiter))] + elseif style === :form + if encoded isa AbstractDict + return explode ? + Pair{String,Any}[String(key) => item for (key, item) in _pairs(encoded)] : + [String(name) => _join_object(encoded, ",", ",")] + elseif encoded isa AbstractVector + return explode ? [String(name) => item for item in encoded] : + [String(name) => _join_array(encoded, ",")] + end + return [String(name) => encoded] + end + throw(ArgumentError("unsupported multipart parameter style $style")) +end + +function _string_keyed_map(value, label) + value isa AbstractDict || value isa NamedTuple || throw( + ArgumentError("$label must be a dictionary or named tuple"), + ) + output = Dict{String,Any}() + for (key, item) in pairs(value) + name = String(key) + haskey(output, name) && throw( + ArgumentError("$label contains duplicate key $(repr(name))"), + ) + output[name] = item + end + return output +end + +function _multipart_encoding_headers(client, encoding, supplied) + supplied_map = _string_keyed_map(supplied, "multipart part headers") + documented = Dict(lowercase(header.name) => header for header in encoding.headers) + values = Dict{String,Any}() + for (name, value) in supplied_map + lowered = lowercase(name) + haskey(values, lowered) && throw( + ArgumentError( + "multipart part headers contain duplicate case-insensitive name $(repr(name))", + ), + ) + descriptor = get(documented, lowered, nothing) + descriptor === nothing && throw( + ArgumentError( + "multipart header $(repr(name)) is not documented for part $(repr(encoding.name))", + ), + ) + values[lowered] = value + end + + output = Pair{String,String}[] + for descriptor in encoding.headers + lowered = lowercase(descriptor.name) + if !haskey(values, lowered) + descriptor.required && throw( + ArgumentError( + "required multipart header $(repr(descriptor.name)) is missing for part $(repr(encoding.name))", + ), + ) + continue + end + value = values[lowered] + _validate_schema(client.spec, + descriptor.schema, + _encode(value), + "encoding multipart header $(descriptor.name)"; + direction = :input, + ) + serialized = if isempty(descriptor.content) + _header_parameter(value, descriptor.explode) + else + text, binary = _parameter_content_value( + client, + first(descriptor.content), + value, + ) + binary && throw( + ArgumentError( + "multipart header content for $(repr(descriptor.name)) must encode as text", + ), + ) + text + end + push!(output, _safe_header(descriptor.name, serialized)) + end + return output +end + +function _multipart_part_headers(value) + value isa MultipartPartHeaders && return value.values, value.parts + return value, NamedTuple() +end + +function _merge_multipart_headers(upload_headers, encoding_headers) + output = Pair{String,String}[] + seen = Set{String}() + for (name, value) in Iterators.flatten((upload_headers, encoding_headers)) + lowered = lowercase(String(name)) + lowered in ("content-type", "content-disposition") && throw( + ArgumentError( + "multipart part headers must not override Content-Type or Content-Disposition", + ), + ) + lowered in seen && throw( + ArgumentError( + "multipart part contains duplicate case-insensitive header $(repr(name))", + ), + ) + push!(seen, lowered) + push!(output, _safe_header(name, value)) + end + return output +end + +function _multipart_body( + client, + value, + media_type, + encodings, + multipart_headers, +) + boundary = replace(string(UUIDs.uuid4()), "-" => "") + configured = Dict(encoding.name => encoding for encoding in encodings) + supplied = _string_keyed_map(multipart_headers, "multipart_headers") + used_supplied = Set{String}() + io = IOBuffer() + for (name, raw_item) in _form_fields(value) + encoding = get(configured, name, nothing) + fields = if encoding !== nothing && encoding.rfc6570 + _multipart_style_pairs( + name, + raw_item, + something(encoding.style, :form), + something(encoding.explode, true), + ) + else + items = raw_item isa AbstractVector && + !(raw_item isa AbstractVector{UInt8}) ? raw_item : (raw_item,) + Pair{String,Any}[String(name) => item for item in items] + end + configured_type = encoding === nothing || encoding.rfc6570 ? nothing : + encoding.content_type + supplied_configuration = get(supplied, name, NamedTuple()) + haskey(supplied, name) && push!(used_supplied, name) + supplied_headers, nested_headers = + _multipart_part_headers(supplied_configuration) + declared_headers = if encoding === nothing + isempty(_string_keyed_map(supplied_headers, "multipart part headers")) || throw( + ArgumentError( + "multipart headers were supplied for undocumented part $(repr(name))", + ), + ) + Pair{String,String}[] + else + _multipart_encoding_headers(client, encoding, supplied_headers) + end + nested_encodings = encoding === nothing ? () : encoding.encoding + if encoding === nothing && + !isempty(_string_keyed_map(nested_headers, "nested multipart_headers")) + throw( + ArgumentError( + "nested multipart headers were supplied for undocumented part $(repr(name))", + ), + ) + end + for (part_name, item) in fields + content, filename, content_type, upload_headers = + _multipart_content( + client, + item, + configured_type, + nested_encodings, + nested_headers, + ) + headers = _merge_multipart_headers(upload_headers, declared_headers) + safe_name = _multipart_quoted_text(part_name, "part name") + write(io, "--", boundary, "\r\n") + write(io, "Content-Disposition: form-data; name=", Char(34), safe_name, Char(34)) + if filename !== nothing + safe_filename = _multipart_quoted_text(filename, "filename") + write(io, "; filename=", Char(34), safe_filename, Char(34)) + end + write(io, "\r\nContent-Type: ", _safe_multipart_text(content_type, "content type"), "\r\n") + for (header, header_value) in headers + write( + io, + _safe_multipart_text(header, "header name"), + ": ", + _safe_multipart_text(header_value, "header value"), + "\r\n", + ) + end + write(io, "\r\n", content, "\r\n") + end + end + unused = setdiff(Set(keys(supplied)), used_supplied) + isempty(unused) || throw( + ArgumentError( + "multipart headers were supplied for absent parts: " * + join(repr.(sort!(collect(unused))), ", "), + ), + ) + write(io, "--", boundary, "--\r\n") + return take!(io), String(media_type) * "; boundary=" * boundary +end + + +function _encode_body( + client, + value, + media_type, + encodings; + multipart_headers = NamedTuple(), +) + media = _base_media_type(media_type) + if !startswith(media, "multipart/") + isempty(_string_keyed_map(multipart_headers, "multipart_headers")) || throw( + ArgumentError( + "multipart_headers can only be used with a multipart request body", + ), + ) + end + if _is_json_media(media) + return JSON.json(_encode(value)), media_type + elseif _is_sequential_json_media(media) + return _encode_sequential_json(value, media), media_type + elseif media == "application/x-www-form-urlencoded" + pairs = _form_pairs(client, value, encodings) + body = join( + ( + _form_escape(key) * "=" * + (preencoded ? item : replace( + _escape(item; allow_reserved), + "%20" => "+", + )) for (key, item, allow_reserved, preencoded) in pairs + ), + '&', + ) + return body, media_type + elseif startswith(media, "multipart/") + return _multipart_body( + client, + value, + media_type, + encodings, + multipart_headers, + ) + elseif startswith(media, "text/") + return value isa AbstractString ? String(value) : _scalar(value), media_type + end + return _body_bytes(_encoded_media_value(client, value, media_type), media_type), media_type +end + +function _server_for(client::Client, operation) + if client.server !== nothing + return String(rstrip(client.server, '/')) + end + if client.is_default && client.spec.server[] != client.spec.default_server + return String(rstrip(client.spec.server[], '/')) + end + servers = operation.servers + isempty(servers) && return client.spec.default_server + selected = if client.server_name === nothing + client.server_index <= length(servers) || throw( + ArgumentError( + "server index $(client.server_index) exceeds the $(length(servers)) documented servers for $(operation.id)", + ), + ) + servers[client.server_index] + else + index = findfirst(server -> server.name == client.server_name, servers) + index === nothing && throw( + ArgumentError( + "server name $(repr(client.server_name)) is not documented for $(operation.id)", + ), + ) + servers[index] + end + url = selected.url + for variable in selected.variables + value = get(client.server_variables, variable.name, variable.default) + isempty(variable.values) || value in variable.values || throw( + ArgumentError( + "server variable $(repr(variable.name)) value $(repr(value)) is not in its documented enum", + ), + ) + url = replace(url, "{" * variable.name * "}" => value) + end + occursin('{', url) && throw( + ArgumentError("not all server URL variables were supplied for $(operation.id)"), + ) + if startswith(lowercase(url), "http://") || startswith(lowercase(url), "https://") + return String(rstrip(url, '/')) + end + base = selected.base + if startswith(lowercase(base), "http://") || startswith(lowercase(base), "https://") + resolved = SchemaEngine.Resources.URIs.resolvereference( + SchemaEngine.Resources.URIs.URI(base), + SchemaEngine.Resources.URIs.URI(url), + ) + return String(rstrip(string(resolved), '/')) + end + return String(rstrip(client.spec.default_server * (startswith(url, '/') ? url : "/" * url), '/')) +end + +# Deployed servers routinely omit or misreport Content-Type, so decode by +# status alone unless several documented media types make the choice ambiguous. +# Returns the selected media entry and the media type to decode the body as. +function _selected_media_entry(operation_id, status, descriptor, received) + selected = isempty(received) ? nothing : + _select_media(descriptor.media, received) + selected === nothing || return selected, String(received) + if isempty(received) || length(descriptor.media) == 1 + entry = first(descriptor.media) + return entry, entry[1] + end + throw( + UnexpectedContentType( + operation_id, + status, + received, + Tuple(first.(descriptor.media)), + ), + ) +end + +function _finish_buffered_response( + client::Client, + operation, + status::Int, + response_headers, + received, + bytes::Vector{UInt8}, + with_http_info::Bool, +) + descriptor = _select_response(operation.responses, status) + decoded = nothing + decoded_headers = Dict{String,Any}() + decode_error = nothing + try + decoded_headers = _decode_response_headers(client, descriptor, response_headers) + if descriptor !== nothing + if isempty(descriptor.media) + if !isempty(bytes) && 200 <= status < 300 + throw(UnexpectedBody(operation.id, status, length(bytes))) + end + decoded = nothing + else + selected, actual_media = + _selected_media_entry(operation.id, status, descriptor, received) + decoded = _decode_body( + client, + selected[2], + actual_media, + bytes, + selected[3], + ) + end + else + # The response status is not documented. Specs regularly leave + # data-less statuses (204, redirects) out, so a successful call + # must not fail here: surface an empty body as `nothing` and any + # payload as raw bytes. + decoded = isempty(bytes) ? nothing : copy(bytes) + end + catch error + 200 <= status < 300 && rethrow() + decode_error = error + end + 200 <= status < 300 || throw( + ApiError( + operation.id, + status, + response_headers, + decoded_headers, + bytes, + decoded, + decode_error, + ), + ) + return with_http_info ? + ApiResponse(status, response_headers, decoded_headers, decoded) : + decoded +end + +# ── streaming responses ────────────────────────────────────────────────────── +# +# Passing `stream_to::Channel` to an operation delivers the response body +# incrementally: the call returns as soon as the response head arrives, a +# background task splits the body into items, decodes each one, and `put!`s it +# on the channel. The channel is closed when the response ends, or closed with +# the error when splitting or decoding fails. Closing the channel from the +# consumer side aborts the transfer. + +# How the body is split into items, and what each item decodes to: +# `:json` re-uses the documented response schema per item (concatenated or +# newline-separated JSON documents, e.g. watch-style endpoints); sequential +# JSON media types split records and decode each to the documented array's +# element type; `text/*` yields lines; anything else yields raw byte chunks. +# A registered decoder receives each framed item and its actual Content-Type. +# Its return value is delivered directly, so it can override an inaccurate +# response schema such as a Kubernetes List declaration on a watch stream. +function _stream_plan(media, type, schema) + if _is_sequential_json_media(media) + item_type = type <: AbstractVector && type !== Vector{UInt8} ? + eltype(type) : type + kind = media == "application/json-seq" || endswith(media, "+json-seq") ? + :jsonseq : :jsonlines + # The documented schema describes the whole sequence, not one record, + # so per-item validation is skipped. + return kind, item_type, nothing + elseif _is_json_media(media) + return :json, type, schema + elseif startswith(media, "text/") || type === String + return :text, type, nothing + end + return :bytes, Vector{UInt8}, nothing +end + +_stream_whitespace(byte::UInt8) = + byte == 0x20 || byte == 0x09 || byte == 0x0d || byte == 0x0a + +function _next_stream_line!(buffer::Vector{UInt8}, final::Bool) + while !isempty(buffer) + index = findfirst(==(0x0a), buffer) + if index === nothing + final || return nothing + frame = copy(buffer) + empty!(buffer) + else + frame = buffer[1:index-1] + deleteat!(buffer, 1:index) + end + while !isempty(frame) && frame[end] == 0x0d + pop!(frame) + end + isempty(frame) || return frame + end + return nothing +end + +# Extract the next complete item from `buffer`, or return `nothing` when more +# bytes are needed. `final` marks the end of the response body. +function _next_stream_frame!(buffer::Vector{UInt8}, kind::Symbol, final::Bool) + if kind === :jsonseq + # RFC 7464: records start with RS and may contain unescaped newlines, + # so a record is complete at the next RS or at the end of the body. + while true + start = 1 + while start <= length(buffer) && + (buffer[start] == 0x1e || _stream_whitespace(buffer[start])) + start += 1 + end + start > 1 && deleteat!(buffer, 1:start-1) + isempty(buffer) && return nothing + index = findfirst(==(0x1e), buffer) + if index === nothing + final || return nothing + frame = copy(buffer) + empty!(buffer) + else + frame = buffer[1:index-1] + deleteat!(buffer, 1:index-1) + end + while !isempty(frame) && _stream_whitespace(frame[end]) + pop!(frame) + end + isempty(frame) || return frame + end + end + kind === :json || return _next_stream_line!(buffer, final) + start = 1 + while start <= length(buffer) && + (_stream_whitespace(buffer[start]) || buffer[start] == 0x1e) + start += 1 + end + start > 1 && deleteat!(buffer, 1:start-1) + isempty(buffer) && return nothing + open_byte = buffer[1] + if open_byte == UInt8('{') || open_byte == UInt8('[') + close_byte = open_byte == UInt8('{') ? UInt8('}') : UInt8(']') + depth = 0 + in_string = false + escaped = false + for (index, byte) in enumerate(buffer) + if escaped + escaped = false + elseif in_string + byte == UInt8('\\') && (escaped = true) + byte == UInt8('"') && (in_string = false) + elseif byte == UInt8('"') + in_string = true + elseif byte == open_byte + depth += 1 + elseif byte == close_byte + depth -= 1 + if depth == 0 + frame = buffer[1:index] + deleteat!(buffer, 1:index) + return frame + end + end + end + return nothing + end + # top-level scalar items (numbers, strings, booleans) end at a newline + return _next_stream_line!(buffer, final) +end + +function _decode_stream_item( + client::Client, + frame::Vector{UInt8}, + kind::Symbol, + item_type, + schema, + media_type, +) + decoder = _select_media_codec(client.media_decoders, media_type) + decoder === nothing || return decoder(copy(frame), String(media_type)) + if kind === :text + isvalid(String, frame) || + throw(DecodeError("streaming text response is not UTF-8")) + return _decode(item_type, String(frame), client.validate_responses) + end + kind === :bytes && return copy(frame) + value = _parse_json(frame, "decoding a streaming response item") + schema === nothing || !client.validate_responses || _validate_schema(client.spec, + schema, + value, + "decoding a streaming response item"; + direction = :output, + ) + return _decode(item_type, value, client.validate_responses) +end + +function _drain_stream_buffer!( + client::Client, + channel::Channel, + buffer::Vector{UInt8}, + kind::Symbol, + item_type, + schema, + media_type, + final::Bool, +) + while true + frame = _next_stream_frame!(buffer, kind, final) + frame === nothing && break + put!( + channel, + _decode_stream_item(client, frame, kind, item_type, schema, media_type), + ) + end + final && kind === :json && !all(_stream_whitespace, buffer) && + throw(DecodeError("streaming response ended with a truncated item")) + return nothing +end + +end # module Runtime diff --git a/src/servergen.jl b/src/servergen.jl index e2f3734..9c069e9 100644 --- a/src/servergen.jl +++ b/src/servergen.jl @@ -1,7 +1,34 @@ # Server-stub generation. The framework-neutral request engine below is pasted -# into generated server modules after GENERATED_RUNTIME_COMMON; framework -# packages (HTTP.jl through OpenAPIHTTPExt, Servo.jl through ServoOpenAPIExt) -# add `OpenAPI.server_source` methods that wrap it with router glue. +# into generated server modules, which import the shared decode/validate +# machinery from OpenAPI.Runtime; framework packages (HTTP.jl through +# OpenAPIHTTPExt, Servo.jl through ServoOpenAPIExt) add `OpenAPI.server_source` +# methods that wrap it with router glue. + +# Emitted at the top of every generated server module. +const GENERATED_SERVER_IMPORTS = raw""" +const Runtime = OpenAPI.Runtime +const SchemaEngine = OpenAPI.SchemaEngine +import OpenAPI.Runtime: + ABSENT, Absent, DecodeError, MultipartPartHeaders, SchemaValidationError, + UnsupportedMediaType, Upload, + _base_media_type, _decode, _decode_schema_header, _decode_sequential_json, + _encode, _encode_sequential_json, _form_fields, _header_atom, _header_scalar, + _header_type_variant, _header_values, _is_json_media, _is_sequential_json_media, + _media_type, _object, _parse_json, _required, _safe_header, _schema_valid, + _select_media, _validate_schema +""" + +# Emitted after the schema data constants; packages the document-specific +# validation data for the shared runtime. +const GENERATED_SERVER_SPEC = raw""" +const _SPEC = Runtime.Spec(; + security_schemes = _SECURITY_SCHEMES, + resources = _SCHEMA_RESOURCE_DATA, + roots = _SCHEMA_ROOT_DATA, + dialects = _SCHEMA_DIALECT_DATA, + directional_required = _SCHEMA_DIRECTIONAL_REQUIRED, +) +""" const GENERATED_RUNTIME_SERVER = raw""" struct _RequestFailure <: Exception @@ -102,7 +129,7 @@ function _object_from_alternating(tokens, context; decode::Bool = false) end function _finish_parameter(descriptor, value, context) - _validate_schema(descriptor.schema, _encode(value), context; direction = :input) + _validate_schema(_SPEC, descriptor.schema, _encode(value), context; direction = :input) return _decode(descriptor.type, value) end @@ -111,10 +138,10 @@ function _decode_parameter_content(descriptor, text, context) media = _base_media_type(entry[1]) if _is_json_media(media) value = _parse_json(text, context) - _validate_schema(entry[3], value, context; direction = :input) + _validate_schema(_SPEC, entry[3], value, context; direction = :input) return _decode(descriptor.type, value) end - _validate_schema(entry[3], text, context; direction = :input) + _validate_schema(_SPEC, entry[3], text, context; direction = :input) return _decode(descriptor.type, text) end @@ -331,7 +358,7 @@ function _decode_header_parameter(descriptor, headers) context = string("decoding header parameter ", descriptor.name) isempty(descriptor.content) || return _decode_parameter_content(descriptor, join(values, ','), context) - return _decode_schema_header( + return _decode_schema_header(_SPEC, descriptor.type, values, descriptor.shape, @@ -486,11 +513,11 @@ function _decode_request_body(operation, headers, body, parts) parts === nothing && throw(_RequestFailure(400, "invalid multipart request body")) value = _multipart_body_object(parts, encodings, fields) - _validate_schema(schema, _encode(value), context; direction = :input) + _validate_schema(_SPEC, schema, _encode(value), context; direction = :input) return (true, _decode(type, value)) elseif media == "application/x-www-form-urlencoded" value = _form_body_object(body, encodings, fields) - _validate_schema(schema, value, context; direction = :input) + _validate_schema(_SPEC, schema, value, context; direction = :input) return (true, _decode(type, value)) elseif _is_json_media(media) if isempty(body) @@ -499,20 +526,20 @@ function _decode_request_body(operation, headers, body, parts) return (false, nothing) end value = _parse_json(body, context) - _validate_schema(schema, value, context; direction = :input) + _validate_schema(_SPEC, schema, value, context; direction = :input) return (true, _decode(type, value)) elseif _is_sequential_json_media(media) value = _decode_sequential_json(body, media) - _validate_schema(schema, value, context; direction = :input) + _validate_schema(_SPEC, schema, value, context; direction = :input) return (true, _decode(type, value)) elseif startswith(media, "text/") || type === String isvalid(String, body) || throw(_RequestFailure(400, "text request body is not valid UTF-8")) value = String(copy(body)) - _validate_schema(schema, value, context; direction = :input) + _validate_schema(_SPEC, schema, value, context; direction = :input) return (true, _decode(type, value)) end - _validate_schema(schema, Base64.base64encode(body), context; direction = :input) + _validate_schema(_SPEC, schema, Base64.base64encode(body), context; direction = :input) return (true, type === Any ? copy(body) : _decode(type, copy(body))) end @@ -670,11 +697,11 @@ function _server_response(operation, result) context = string("encoding the ", operation.id, " response body") if _is_json_media(media) lowered = _encode(result) - _validate_schema(entry[3], lowered, context; direction = :output) + _validate_schema(_SPEC, entry[3], lowered, context; direction = :output) payload = Vector{UInt8}(codeunits(JSON.json(lowered))) elseif _is_sequential_json_media(media) lowered = _encode(result) - _validate_schema(entry[3], lowered, context; direction = :output) + _validate_schema(_SPEC, entry[3], lowered, context; direction = :output) payload = Vector{UInt8}(codeunits(_encode_sequential_json(result, media))) elseif startswith(media, "text/") result isa AbstractString || throw(ArgumentError(string( @@ -682,7 +709,7 @@ function _server_response(operation, result) operation.id, " documents a text response; return an AbstractString or a framework response", ))) - _validate_schema(entry[3], String(result), context; direction = :output) + _validate_schema(_SPEC, entry[3], String(result), context; direction = :output) payload = Vector{UInt8}(codeunits(String(result))) else bytes = result isa AbstractVector{UInt8} ? Vector{UInt8}(result) : @@ -692,7 +719,7 @@ function _server_response(operation, result) operation.id, " documents a binary response; return bytes, a string, or a framework response", ))) - _validate_schema(entry[3], Base64.base64encode(bytes), context; direction = :output) + _validate_schema(_SPEC, entry[3], Base64.base64encode(bytes), context; direction = :output) payload = bytes end headers = Pair{String,String}[_safe_header("Content-Type", entry[1])] @@ -820,11 +847,10 @@ function server_module_source( println(io, "module ", plan.module_name, "\n") println(io, imports) plan.datetime === :zoned && println(io, "using TimeZones") - println(io, "const SchemaEngine = OpenAPI.SchemaEngine\n") + print(io, GENERATED_SERVER_IMPORTS, '\n') _emit_security(io, plan) _emit_schema_data(io, plan) - print(io, GENERATED_RUNTIME_COMMON, '\n') - plan.datetime === :zoned && print(io, GENERATED_ZONED_RUNTIME, '\n') + print(io, GENERATED_SERVER_SPEC, '\n') print(io, GENERATED_RUNTIME_SERVER, '\n') indices = _model_indices(plan) wrapped_aliases = _cyclic_aliases(plan) diff --git a/test/discriminators.jl b/test/discriminators.jl index 6d02c7e..46fa6b4 100644 --- a/test/discriminators.jl +++ b/test/discriminators.jl @@ -81,7 +81,7 @@ response_media = only(only(client_module._OP_getpet.responses).media) decode_pet(json) = Base.invokelatest( - client_module._decode_body, + OpenAPI.Runtime._decode_body, client_module.DEFAULT_CLIENT, client_module.Pet, "application/json", diff --git a/test/models.jl b/test/models.jl index 99811d9..fbedc7d 100644 --- a/test/models.jl +++ b/test/models.jl @@ -140,12 +140,12 @@ call(name, args...) = Base.invokelatest(getfield(C, name), args...) media(name) = only(only(getfield(C, Symbol("_OP_", name)).responses).media) - @test call(:_schema_valid, media("truevalue")[3], Dict("anything" => 1)) - @test !call(:_schema_valid, media("falsevalue")[3], nothing) - @test call(:_schema_valid, media("constseven")[3], 7) - @test !call(:_schema_valid, media("constseven")[3], 8) - @test call(:_schema_valid, media("forbiddenstring")[3], 1) - @test !call(:_schema_valid, media("forbiddenstring")[3], "no") + @test call(:_schema_valid, C._SPEC, media("truevalue")[3], Dict("anything" => 1)) + @test !call(:_schema_valid, C._SPEC, media("falsevalue")[3], nothing) + @test call(:_schema_valid, C._SPEC, media("constseven")[3], 7) + @test !call(:_schema_valid, C._SPEC, media("constseven")[3], 8) + @test call(:_schema_valid, C._SPEC, media("forbiddenstring")[3], 1) + @test !call(:_schema_valid, C._SPEC, media("forbiddenstring")[3], "no") @test call(:_decode, C.MixedEnum, "x").value == "x" @test call(:_decode, C.MixedEnum, nothing).value === nothing diff --git a/test/normalization.jl b/test/normalization.jl index 443bef4..a41cbad 100644 --- a/test/normalization.jl +++ b/test/normalization.jl @@ -81,9 +81,9 @@ end client_module = Base.invokelatest(getfield, host, :NullableClient) response = only(client_module._OP_getnullable.responses) media = only(response.media) - @test Base.invokelatest(client_module._schema_valid, media[3], nothing) + @test Base.invokelatest(client_module._schema_valid, client_module._SPEC, media[3], nothing) @test Base.invokelatest( - client_module._decode_body, + OpenAPI.Runtime._decode_body, client_module.DEFAULT_CLIENT, Union{Nothing,String}, "application/json", @@ -226,15 +226,15 @@ end ) rules = Base.invokelatest(getfield, constrained_host, :NullableRulesClient) descriptor(name) = only(only(getfield(rules, Symbol("_OP_", name)).responses).media)[3] - @test Base.invokelatest(rules._schema_valid, descriptor("direct"), nothing) + @test Base.invokelatest(rules._schema_valid, rules._SPEC, descriptor("direct"), nothing) @test !Base.invokelatest( - rules._schema_valid, + rules._schema_valid, rules._SPEC, descriptor("constrained"), nothing, ) - @test !Base.invokelatest(rules._schema_valid, descriptor("composed"), nothing) + @test !Base.invokelatest(rules._schema_valid, rules._SPEC, descriptor("composed"), nothing) instance = Dict("type" => "string", "nullable" => true) - @test Base.invokelatest(rules._schema_valid, descriptor("instance"), instance) + @test Base.invokelatest(rules._schema_valid, rules._SPEC, descriptor("instance"), instance) @test Nothing <: rules.DirectNullable @test !(Nothing <: rules.Constrained) @test !(Nothing <: rules.Composed) @@ -261,7 +261,7 @@ end only(permissive._OP_composed.responses).media, )[3] @test Base.invokelatest( - permissive._schema_valid, + permissive._schema_valid, permissive._SPEC, composed_descriptor, nothing, ) @@ -269,7 +269,7 @@ end only(permissive._OP_constrained.responses).media, )[3] @test !Base.invokelatest( - permissive._schema_valid, + permissive._schema_valid, permissive._SPEC, constrained_descriptor, nothing, ) @@ -282,7 +282,7 @@ end ).value === nothing choice_descriptor = only(only(permissive._OP_choice.responses).media)[3] @test Base.invokelatest( - permissive._schema_valid, + permissive._schema_valid, permissive._SPEC, choice_descriptor, nothing, ) @@ -484,7 +484,7 @@ end ) response_media = only(only(client_module._OP_roundtrip.responses).media) output = Base.invokelatest( - client_module._decode_body, + OpenAPI.Runtime._decode_body, client_module.DEFAULT_CLIENT, client_module.DirectionalOutput, "application/json", diff --git a/test/references.jl b/test/references.jl index 69a0c07..21ef89b 100644 --- a/test/references.jl +++ b/test/references.jl @@ -144,8 +144,8 @@ name_media = only(only(Strict._OP_getexternalname.responses).media) legacy_media = only(only(Strict._OP_getexternallegacy.responses).media) @test name_media[2] == Union{Nothing,String} - @test Base.invokelatest(Strict._schema_valid, name_media[3], nothing) - @test !Base.invokelatest(Strict._schema_valid, legacy_media[3], nothing) + @test Base.invokelatest(Strict._schema_valid, Strict._SPEC, name_media[3], nothing) + @test !Base.invokelatest(Strict._schema_valid, Strict._SPEC, legacy_media[3], nothing) permissive = OpenAPI.normalize(root_path; strict = false) @test any( @@ -172,7 +172,7 @@ ) @test Nothing <: permissive_media[2] @test Base.invokelatest( - Permissive._schema_valid, + Permissive._schema_valid, Permissive._SPEC, permissive_media[3], nothing, ) diff --git a/test/runtime.jl b/test/runtime.jl index 7f58e19..da33942 100644 --- a/test/runtime.jl +++ b/test/runtime.jl @@ -18,8 +18,12 @@ Base.include_string(host, source, "RuntimeUnitClient.jl") C = Base.invokelatest(getfield, host, :RuntimeUnitClient) - invoke(function_name, args...; kwargs...) = - Base.invokelatest(getfield(C, function_name), args...; kwargs...) + invoke(function_name, args...; kwargs...) = Base.invokelatest( + isdefined(C, function_name) ? getfield(C, function_name) : + getfield(OpenAPI.Runtime, function_name), + args...; + kwargs..., + ) @testset "path styles" begin @test invoke(:_path_parameter, "id", ["a", "b"], :simple, false) == "a,b" diff --git a/test/runtime_integration.jl b/test/runtime_integration.jl index 56e4629..8cde077 100644 --- a/test/runtime_integration.jl +++ b/test/runtime_integration.jl @@ -799,7 +799,12 @@ end C = Base.invokelatest(getfield, host, :RuntimeHTTPClient) call(name, args...; kwargs...) = - Base.invokelatest(getfield(C, name), args...; kwargs...) + Base.invokelatest( + isdefined(C, name) ? getfield(C, name) : + getfield(OpenAPI.Runtime, name), + args...; + kwargs..., + ) take_request() = take!(captures) client = C.Client() From c2a524422b00809011af0189d5ac87105cff51b6 Mon Sep 17 00:00:00 2001 From: Jacob Quinn Date: Tue, 11 Aug 2026 15:09:39 -0600 Subject: [PATCH 9/9] perf(client): precompile generation pipeline Move OpenAPI loading, validation, planning, and generation compilation into the package images. Re-run the workload after the HTTP extension loads so its method additions do not restore first-use latency. Replace tuple-length-specialized generator expressions in planning and source emission with stable typed loops. This reduces the Soleil cold client setup from about 25 seconds to about 1.8 seconds while preserving generated output exactly. --- Project.toml | 2 + ext/OpenAPIHTTPExt.jl | 18 +++++++++ src/OpenAPI.jl | 1 + src/client.jl | 74 +++++++++++++++++++++++++++-------- src/planning.jl | 19 +++++++-- src/precompile.jl | 90 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 183 insertions(+), 21 deletions(-) create mode 100644 src/precompile.jl diff --git a/Project.toml b/Project.toml index 5d9b623..b1e87b3 100644 --- a/Project.toml +++ b/Project.toml @@ -10,6 +10,7 @@ version = "1.0.0" Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" @@ -28,6 +29,7 @@ Base64 = "1.11" Dates = "1.11" HTTP = "2" JSON = "1.7" +PrecompileTools = "1" TimeZones = "1" URIs = "1" UUIDs = "1.11" diff --git a/ext/OpenAPIHTTPExt.jl b/ext/OpenAPIHTTPExt.jl index fa8a501..8e12881 100644 --- a/ext/OpenAPIHTTPExt.jl +++ b/ext/OpenAPIHTTPExt.jl @@ -5,6 +5,7 @@ module OpenAPIHTTPExt using OpenAPI, HTTP +using PrecompileTools: @compile_workload struct _ResponseTooLarge <: Exception end @@ -538,4 +539,21 @@ function _stream_request( stream_to end +# HTTP adds methods to OpenAPI's transport seams after the core package image +# is loaded. Re-run the small client workload in this final method world so the +# extension does not leave loading and generation work for the first request. +precompile( + OpenAPI.fetchresource, + (OpenAPI.Resources.ResourceId, Int), +) + +@compile_workload begin + precompile_source = OpenAPI.read( + OpenAPI._PRECOMPILE_CLIENT_DOCUMENT; + base_uri = "https://precompile.openapi.invalid/openapi.json", + ) + null_path = Sys.iswindows() ? "NUL" : "/dev/null" + OpenAPI.client(precompile_source; name = "PrecompileHTTPClient", path = null_path) +end + end # module diff --git a/src/OpenAPI.jl b/src/OpenAPI.jl index 575ce88..c0ee037 100644 --- a/src/OpenAPI.jl +++ b/src/OpenAPI.jl @@ -40,6 +40,7 @@ include("read.jl") include("runtime.jl") include("client.jl") include("servergen.jl") +include("precompile.jl") # ── extension seams ───────────────────────────────────────────────────────── diff --git a/src/client.jl b/src/client.jl index 2930c2d..2bf80c4 100644 --- a/src/client.jl +++ b/src/client.jl @@ -182,7 +182,13 @@ function _emit_security(io::IO, plan::GenerationPlan) end function _model_indices(plan::GenerationPlan) - return Dict(model.name => index for (index, model) in enumerate(plan.models)) + indices = Dict{String,Int}() + index = 0 + for model in plan.models + index += 1 + indices[model.name] = index + end + return indices end function _model_references(type::String, names) @@ -196,11 +202,16 @@ function _model_references(type::String, names) end function _cyclic_aliases(plan::GenerationPlan) - aliases = Set(model.name for model in plan.models if model.kind === :alias) - edges = Dict( - model.name => _model_references(something(model.alias, ""), aliases) for - model in plan.models if model.kind === :alias - ) + aliases = Set{String}() + for model in plan.models + model.kind === :alias && push!(aliases, model.name) + end + edges = Dict{String,Vector{String}}() + for model in plan.models + model.kind === :alias || continue + edges[model.name] = + _model_references(something(model.alias, ""), aliases) + end function reaches(start, current, seen) current in seen && return false push!(seen, current) @@ -210,12 +221,23 @@ function _cyclic_aliases(plan::GenerationPlan) end return false end - return Set(name for name in aliases if reaches(name, name, Set{String}())) + cyclic = Set{String}() + for name in aliases + reaches(name, name, Set{String}()) && push!(cyclic, name) + end + return cyclic end function _model_types(model::ModelPlan, wrapped_aliases) if model.kind === :object - types = String[field.type for field in model.fields] + types = String[] + sizehint!( + types, + length(model.fields) + (model.additional_type === nothing ? 0 : 1), + ) + for field in model.fields + push!(types, field.type) + end model.additional_type === nothing || push!(types, model.additional_type) return types elseif model.kind in (:oneof, :anyof) @@ -228,12 +250,19 @@ end function _forward_abstracts(plan::GenerationPlan, wrapped_aliases) indices = _model_indices(plan) - concrete = Set(model.name for model in plan.models if model.kind !== :alias) + concrete = Set{String}() + for model in plan.models + model.kind === :alias || push!(concrete, model.name) + end union!(concrete, wrapped_aliases) targets = copy(wrapped_aliases) - for (index, model) in enumerate(plan.models) - for type in _model_types(model, wrapped_aliases), target in _model_references(type, concrete) - get(indices, target, 0) > index && push!(targets, target) + index = 0 + for model in plan.models + index += 1 + for type in _model_types(model, wrapped_aliases) + for target in _model_references(type, concrete) + get(indices, target, 0) > index && push!(targets, target) + end end end return targets @@ -501,7 +530,11 @@ function _emit_model( ) end end - known = Tuple(field.wire_name for field in model.fields) + known = String[] + sizehint!(known, length(model.fields)) + for field in model.fields + push!(known, field.wire_name) + end if model.additional_type !== nothing additional_type = _rewrite_forward(model.additional_type, index, indices, abstract_targets) println(io, " _openapi_additional_properties = Dict{String,", additional_type, "}()") @@ -514,9 +547,14 @@ function _emit_model( println(io, " !_openapi_validate || isempty(_openapi_unknown) || throw(DecodeError(\"unknown fields while decoding ", model.name, ": \" * join(_openapi_unknown, \", \")))") end print(io, " return ", model.name, "(") - assignments = String[ - string(field.name, " = _openapi_field_", field.name) for field in model.fields - ] + assignments = String[] + sizehint!( + assignments, + length(model.fields) + (model.additional_type === nothing ? 0 : 1), + ) + for field in model.fields + push!(assignments, string(field.name, " = _openapi_field_", field.name)) + end model.additional_type === nothing || push!(assignments, "additional_properties = _openapi_additional_properties") println(io, "; ", join(assignments, ", "), ")") println(io, "end") @@ -1215,7 +1253,9 @@ function _generate(plan::ClientPlan) println(io, "abstract type Abstract", target, " end") end isempty(abstract_targets) || println(io) - for (index, model) in enumerate(plan.models) + index = 0 + for model in plan.models + index += 1 _emit_model( io, model, diff --git a/src/planning.jl b/src/planning.jl index ee5d22d..e5132c9 100644 --- a/src/planning.jl +++ b/src/planning.jl @@ -1555,20 +1555,31 @@ end function _plan_components!(context::PlanningContext) api = context.api # Reserve stable component names before recursive planning starts. - for (component_name, handle) in sort(collect(api.schemas); by = first) + schemas = Pair{String,SchemaHandle}[] + sizehint!(schemas, length(api.schemas)) + for schema in api.schemas + push!(schemas, schema) + end + sort!(schemas; by = first) + for (component_name, handle) in schemas view = SchemaView(handle) resolved = _resolved_view(view) key = (resolved.node, :neutral) haskey(context.names, key) || (context.names[key] = _allocate_name!(context, component_name)) end - for (component_name, handle) in sort(collect(api.schemas); by = first) + for (component_name, handle) in schemas _type_for!(context, SchemaView(handle), component_name, :neutral) end operations = OperationPlan[] used_functions = Dict{String,Int}() - ordered = sort( - [operation for operation in api.operations if operation.direction === :request]; + ordered = NormalizedOperation[] + sizehint!(ordered, length(api.operations)) + for operation in api.operations + operation.direction === :request && push!(ordered, operation) + end + sort!( + ordered; by = operation -> (operation.path, String(operation.method), operation.id), ) for operation in ordered diff --git a/src/precompile.jl b/src/precompile.jl new file mode 100644 index 0000000..31d0cad --- /dev/null +++ b/src/precompile.jl @@ -0,0 +1,90 @@ +using PrecompileTools: @compile_workload + +# Client generation is normally a one-time startup operation. Exercise a small, +# representative document while OpenAPI precompiles so users do not pay the +# compiler cost when they generate their first client in a new Julia process. +const _PRECOMPILE_CLIENT_DOCUMENT = raw""" +{ + "openapi": "3.2.0", + "info": {"title": "Precompile API", "version": "1.0.0"}, + "components": { + "securitySchemes": { + "BearerAuth": {"type": "http", "scheme": "bearer"} + }, + "schemas": { + "MonthlySummary": { + "type": "object", + "required": ["month", "energy"], + "properties": { + "month": {"type": "integer", "format": "int64"}, + "energy": {"type": "number", "format": "double"} + }, + "additionalProperties": false + }, + "Summary": { + "type": "object", + "required": ["best_day", "monthly"], + "properties": { + "best_day": {"type": "string", "format": "date"}, + "monthly": { + "type": "array", + "items": {"$ref": "#/components/schemas/MonthlySummary"} + } + }, + "additionalProperties": false + } + } + }, + "paths": { + "/simulate/{latitude}": { + "get": { + "operationId": "simulate", + "security": [{"BearerAuth": []}], + "parameters": [ + { + "name": "latitude", + "in": "path", + "required": true, + "schema": {"type": "number", "format": "double"} + }, + { + "name": "capacity", + "in": "query", + "schema": {"type": "number", "format": "double"} + } + ], + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Summary"} + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {"message": {"type": "string"}} + } + } + } + } + } + } + } + } +} +""" + +@compile_workload begin + precompile_source = read( + _PRECOMPILE_CLIENT_DOCUMENT; + base_uri = "https://precompile.openapi.invalid/openapi.json", + ) + null_path = Sys.iswindows() ? "NUL" : "/dev/null" + client(precompile_source; name = "PrecompileClient", path = null_path) +end