From 585580f7a551e8a65f3e16fc85a34386242fd83c Mon Sep 17 00:00:00 2001 From: Tanmay Mohapatra Date: Sat, 12 Aug 2023 13:37:04 +0530 Subject: [PATCH] add docs using documenter (#56) Moved existing docs from readme, and added few more. Using documenter to prepare and publish docs. --- .github/workflows/ci.yml | 11 ++ README.md | 339 ++------------------------------------- docs/.gitignore | 1 + docs/Project.toml | 5 + docs/make.jl | 24 +++ docs/src/index.md | 12 ++ docs/src/reference.md | 65 ++++++++ docs/src/todo.md | 14 ++ docs/src/tools.md | 100 ++++++++++++ docs/src/userguide.md | 193 ++++++++++++++++++++++ src/client.jl | 84 +++++++++- 11 files changed, 514 insertions(+), 334 deletions(-) create mode 100644 docs/.gitignore create mode 100644 docs/Project.toml create mode 100644 docs/make.jl create mode 100644 docs/src/index.md create mode 100644 docs/src/reference.md create mode 100644 docs/src/todo.md create mode 100644 docs/src/tools.md create mode 100644 docs/src/userguide.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a070780..debe8e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,3 +40,14 @@ jobs: - uses: codecov/codecov-action@v1 with: file: lcov.info + docs: + name: Documentation + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-docdeploy@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index ea08fd3..1d1e229 100644 --- a/README.md +++ b/README.md @@ -5,221 +5,18 @@ 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. +[![](https://img.shields.io/badge/docs-stable-blue.svg)](https://JuliaComputing.github.io/OpenAPI.jl/stable) +[![](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaComputing.github.io/OpenAPI.jl/dev) -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. +## Quick Guide -> Note: 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. - ---- -## Table of Contents - -- [Code Generation](#code-generation) -- [Generated Code Structure](#generated-code-structure) - - [Models](#models) - - [Validations](#validations) - - [Client APIs](#client-apis) - - [Server APIs](#server-apis) -- [Examples](#examples) -- [Tools](#tools) - - [Swagger UI](#swagger-ui) - - [Swagger Editor](#swagger-editor) -- [TODO](#todo) ---- - -## 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). - -## Generated Code Structure - -### 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, - verbose::Union{Bool,Function}=false, -) -``` - -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 -- `verbose`: whether to enable verbose logging - -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 `verbose` option can be one of: -- `false`: the default, no verbose logging -- `true`: enables curl verbose logging to stderr -- a function that accepts two arguments - type and message (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` - -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` +- 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. ## Examples @@ -231,119 +28,3 @@ The Petstore is a common example that most OpenAPI implementations use to test a - 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) - -## Tools - -### 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 -# specify 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 -) - -# specify 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 -) -``` - -## 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/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +build diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..fb315d1 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,5 @@ +[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 new file mode 100644 index 0000000..8f38a1f --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,24 @@ +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 new file mode 100644 index 0000000..f2f28b6 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,12 @@ +# 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 new file mode 100644 index 0000000..531854d --- /dev/null +++ b/docs/src/reference.md @@ -0,0 +1,65 @@ +```@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 +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 new file mode 100644 index 0000000..25b3581 --- /dev/null +++ b/docs/src/todo.md @@ -0,0 +1,14 @@ +# 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 new file mode 100644 index 0000000..52c6073 --- /dev/null +++ b/docs/src/tools.md @@ -0,0 +1,100 @@ +# Tools + +## 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 +# specify 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 +) + +# specify 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 new file mode 100644 index 0000000..eb426ff --- /dev/null +++ b/docs/src/userguide.md @@ -0,0 +1,193 @@ +# 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, + verbose::Union{Bool,Function}=false, +) +``` + +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 +- `verbose`: whether to enable verbose logging + +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 `verbose` option can be one of: +- `false`: the default, no verbose logging +- `true`: enables curl verbose logging to stderr +- a function that accepts two arguments - type and message (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` + +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` diff --git a/src/client.jl b/src/client.jl index e5a0944..89ca634 100644 --- a/src/client.jl +++ b/src/client.jl @@ -45,12 +45,15 @@ struct ApiException <: Exception end """ -Represents the raw HTTP provol response from the server. + 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 ( as a Downloads.Response object) +- `status`: the HTTP status code +- `message`: the HTTP status message +- `headers`: the HTTP headers +- `raw`: the raw response ( as a Downloads.Response object) """ struct ApiResponse raw::Downloads.Response @@ -93,6 +96,43 @@ 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, + verbose::Union{Bool,Function}=false, + ) + +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. +- `verbose`: Can be set either to a boolean or a function. + - If set to true, then the client will log all HTTP requests and responses. + - If set to a function, then that function will be called with the following parameters: + - `type`: The type of message. + - `message`: The message to be logged. + +""" struct Client root::String headers::Dict{String,String} @@ -127,9 +167,32 @@ struct Client end end +""" + set_user_agent(client::Client, ua::String) + +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) + +""" + set_cookie(client::Client, ck::String) + +Set the Cookie header to be sent with all API calls. +""" set_cookie(client::Client, ck::String) = set_header(client, "Cookie", ck) + +""" + set_header(client::Client, name::String, value::String) + +Set the specified header to be sent with all API calls. +""" set_header(client::Client, name::String, value::String) = (client.headers[name] = value) + +""" + set_timeout(client::Client, timeout::Int) + +Set the timeout in seconds for all API calls. +""" set_timeout(client::Client, timeout::Int) = (client.timeout[] = timeout) function with_timeout(fn, client::Client, timeout::Integer) @@ -515,6 +578,12 @@ function setproperty!(o::T, name::Symbol, val) where {T<:APIModel} end end +""" + getpropertyat(o::T, path...) where {T<:APIModel} + +Returns the property at the specified path. +The path can be a single property name or a chain of property names separated by dots, representing a nested property. +""" function getpropertyat(o::T, path...) where {T<:APIModel} val = getproperty(o, Symbol(path[1])) rempath = path[2:end] @@ -533,6 +602,11 @@ function getpropertyat(o::T, path...) where {T<:APIModel} getpropertyat(val, rempath...) end +""" + haspropertyat(o::T, path...) where {T<:APIModel} + +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)