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..441bd99 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,159 @@ +# OpenAPI.jl maintainer guide + +## Purpose + +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 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 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 + +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 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. +- 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; the + 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 + +- `SourceDocument`, `Diagnostic`, `OpenAPIError`: loading and diagnostics. +- `NormalizedAPI`: immutable normalized document. +- `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`, `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`, +`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..bd2f884 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,456 @@ -# 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. + +## 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. + +- `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; +- `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. + +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`. + +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 +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. 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 +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..14f6d8b --- /dev/null +++ b/ext/OpenAPIHTTPExt.jl @@ -0,0 +1,163 @@ +# Lets OpenAPI.read fetch documents over HTTP (e.g. a running app's +# /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 + +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, + ), +) + +# ── 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/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..9f882c7 100644 --- a/src/OpenAPI.jl +++ b/src/OpenAPI.jl @@ -1,27 +1,63 @@ -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. +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 -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") +include("servergen.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..6a89a3b 100644 --- a/src/client.jl +++ b/src/client.jl @@ -1,445 +1,3551 @@ -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_COMMON = 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) -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 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 -""" - set_cookie(client::Client, ck::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 Cookie header to be sent with all API calls. -""" -set_cookie(client::Client, ck::String) = set_header(client, "Cookie", ck) +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_header(client::Client, name::String, value::String) +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 specified header to be sent with all API calls. -""" -set_header(client::Client, name::String, value::String) = (client.headers[name] = value) +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 -""" - set_timeout(client::Client, timeout::Int) +_schema_valid(descriptor, value; direction::Symbol = :neutral) = + isempty(_schema_issues(descriptor, value; direction)) -Set the timeout in seconds for all API calls. -""" -set_timeout(client::Client, timeout::Int) = (client.timeout[] = timeout) +struct Upload + data::Vector{UInt8} + filename::Union{Nothing,String} + content_type::Union{Nothing,String} + headers::Vector{Pair{String,String}} +end -function with_timeout(fn, client::Client, timeout::Integer) - oldtimeout = client.timeout[] - client.timeout[] = timeout +# 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 - fn(client) - finally - client.timeout[] = oldtimeout + 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) + +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 + +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 -is_json_mime(mime::T) where {T <: AbstractString} = ("*/*" == mime) || occursin(r"(?i)application/json(;.*)?", mime) || occursin(r"(?i)application/(.*)\+json(;.*)?", mime) +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 -function select_header_accept(accepts::Vector{String}) - isempty(accepts) && (return "application/json") - for accept in accepts - is_json_mime(accept) && (return accept) +_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 join(accepts, ", ") + return selected 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 _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 ctypes[1] + return default 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_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 -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 _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 - return nothing + type <: AbstractFloat && return try + parse(type, text) + catch error + throw(DecodeError("invalid numeric value: $(sprint(showerror, error))")) + end + return _decode(type, text) 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 +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 - false + _header_scalar(selected, join(values, set_cookie ? '\n' : ',')) end + _validate_schema( + schema, + _encode(raw), + context; + direction, + ) + return _decode(type, raw) 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 + +_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 - 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) +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 - elseif !isa(value, Vector) || isempty(collection_format) - params[name] = string(value) else - dlm = get(COLL_DLM, collection_format, ",") - isempty(dlm) && throw(OpenAPIException("Unsupported collection format $collection_format")) - params[name] = join(string.(value), dlm) + for line in eachline(IOBuffer(body)) + text = strip(line) + isempty(text) || push!(records, _parse_json(text, "decoding JSON lines")) + end end + return records end -prep_args(ctx::Ctx) = prep_args(Val(ctx.client.httplib), ctx) - -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 +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 -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 +""" + +# 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 -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 +_encode(value::TimeZones.ZonedDateTime) = + Dates.format(value, TimeZones.ISOZonedDateTimeFormat) +""" -response(::Type{ZonedDateTime}, data) = str2zoneddatetime(data) -response(::Type{DateTime}, data) = str2datetime(data) -response(::Type{Date}, data) = str2date(data) +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 -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] +struct ApiResponse{T} + status::Int + headers::Vector{Pair{String,String}} + decoded_headers::Dict{String,Any} + body::T +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) +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 -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) +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 - # 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 _normalize_media_codecs(codecs::Dict{String,Function}) + normalized = Dict{String,Function}() + for name in keys(codecs) + normalized[_base_media_type(name)] = codecs[name] end - # append query params if needed - if !isempty(ctx.query) - resource_path = string(URIs.URI(URIs.URI(resource_path); query=escapeuri(ctx.query))) + 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) + +_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 - body, kwargs = prep_args(ctx) +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 - # 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 +_path_scalar(value) = _escape(_scalar(value)) - if stream - @assert stream_to !== nothing +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 - output = Base.BufferStream() - resp, output = do_request(Val(ctx.client.httplib), ctx, resource_path, body, output, kwargs, stream; stream_to=stream_to) +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 - return resp, output +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 -function exec(ctx::Ctx, stream_to::Union{Channel,Nothing}=nothing) - stream = stream_to !== nothing - resp, output = do_request(ctx, stream; stream_to=stream_to) +_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 - if resp === nothing - # request was interrupted - throw(InvocationException("request was interrupted")) +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 - if isa(resp, HTTPLibError) - throw(ApiException(resp)) +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 - if stream - return stream_to, ApiResponse(resp) +_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 - 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) + 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 response(return_type, resp, data), ApiResponse(resp) + 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 setproperty!(o::T, name::Symbol, val) where {T<:APIModel} - validate_property(T, name, val) - fieldtype = property_type(T, name) +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 - 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 _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 - return setfield!(o, name, ftval) + 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 -""" - getpropertyat(o::T, path...) where {T<:APIModel} +_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 -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] +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 = 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 + +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 - return [getpropertyat(item, rempath...) for item in val] + 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 - (length(rempath) == 0) && (return val) - getpropertyat(val, rempath...) +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 -""" - haspropertyat(o::T, path...) where {T<:APIModel} +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 -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 +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 - 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) -""" - 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 -""" - is_request_interrupted(ex::Exception) +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)")) + 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) + + 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 -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" +# 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 -""" - storefile(api_call::Function; - folder::AbstractString = pwd(), - rename_file::String="", - )::Tuple{Any,ApiResponse,String} +# ── 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 - Helper method that stores the result of an API call that returns file - contents (as binary or text string) into a file. +_stream_whitespace(byte::UInt8) = + byte == 0x20 || byte == 0x09 || byte == 0x0d || byte == 0x0a - Convenient to use it in a do block. Returns the path where file is stored additionally. +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 - 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") +# 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 - Parameters: +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 - - `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 _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 - Returns: (result, http_response, file_path) +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 storefile(api_call::Function; - folder::AbstractString = pwd(), - filename::Union{String,Nothing} = nothing, - )::Tuple{Any,ApiResponse,String} - result, http_response = api_call() +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 - if isnothing(filename) - filename = extract_filename(http_response) +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 - mkpath(folder) - filepath = joinpath(folder, filename) +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 - open(filepath, "w") do io - write(io, result) +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 - return result, http_response, filepath +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), " => (") + 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 -const content_disposition_re = r"filename\*?=['\"]?(?:UTF-\d['\"]*)?([^;\r\n\"']*)['\"]?;?" +function _model_indices(plan::GenerationPlan) + return Dict(model.name => index for (index, model) in enumerate(plan.models)) +end -""" - extract_filename(resp)::String +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 -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 _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 + ) + 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 + +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 + +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) + 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 + +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 + +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 - push!(parts, "$new_key"=>"$value") + 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 + + 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 - return Dict(parts) + 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 + +function _schema_node(handle::Union{Nothing,SchemaHandle}) + handle === nothing && return nothing + compiled = handle.compiled + return compiled === nothing ? handle.node : compiled.root +end + +function _schema_descriptor(node::Union{Nothing,Resources.NodeId}) + node === nothing && return "nothing" + return "(resource = " * repr(string(node.resource)) * + ", pointer = " * repr(string(node.pointer)) * ")" end -function request_supports_interrupt() - for m in methods(request) - if :interrupt in Base.kwarg_decl(m) - return true +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 false + 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::GenerationPlan, 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::GenerationPlan) + 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 + end + end + 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::GenerationPlan) + 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") * + ", shape = " * repr(_schema_shape(_parameter_schema(parameter.parameter))) * + ", 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 -end # module Clients +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 view +end + +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 ? "," : "") * ")" + # 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(view::SchemaView) + resolved = _resolved_view(view) + _is_object_schema(resolved) && return :object + types = _schema_types(resolved.value) + ( + "array" in types || + 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 + 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()", + "stream_to::Union{Nothing,Channel} = nothing", + ], + ) + 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, stream_to", + ) + 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") + 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)") + 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. + +`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; + 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..ea38921 --- /dev/null +++ b/src/normalize.jl @@ -0,0 +1,1899 @@ +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, + ) + # 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, + "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..3e6973d --- /dev/null +++ b/src/planning.jl @@ -0,0 +1,1681 @@ +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 + datetime::Symbol +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 + datetime::Symbol +end + +const GenerationPlan = Union{ClientPlan,ServerPlan} + +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 + datetime::Symbol +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; + datetime::Symbol = :utc, +) + 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, + 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) + 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 context.datetime === :zoned ? "TimeZones.ZonedDateTime" : + "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 + +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) + 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 + 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, + 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; datetime) + _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( + api, + _type_identifier(name), + Tuple(context.models), + Tuple(operations), + diagnostics, + context.uses_dates, + context.uses_uuids, + context.uses_base64, + context.datetime, + ) +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, + 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; datetime) + _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, + context.datetime, + ) +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/servergen.jl b/src/servergen.jl new file mode 100644 index 0000000..6dbeeaf --- /dev/null +++ b/src/servergen.jl @@ -0,0 +1,869 @@ +# 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) + 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) + 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/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..00faec2 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,19 @@ +[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" +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" + +[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..3e404cd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,227 +1,261 @@ -using Test, HTTP +using Test, OpenAPI, JSON, Dates +import Downloads, HTTP, Sockets, TimeZones -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("servergen.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..26663a1 --- /dev/null +++ b/test/runtime.jl @@ -0,0 +1,349 @@ +@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 "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 "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)] + @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..9582894 --- /dev/null +++ b/test/runtime_integration.jl @@ -0,0 +1,1187 @@ +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, "/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, + ["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, + ) + 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( + "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()), + ), + ), + ), + "/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", + "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"), + ), + ), + ), + ), + ), + "/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", + "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() + # 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 + 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..19e642f --- /dev/null +++ b/test/semantics.jl @@ -0,0 +1,473 @@ +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" => 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 + + # 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 + 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/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 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