API
Macros
RestClient.@globalconfig
— Macro@globalconfig expr
Define globalconfig
for the current module as expr
.
This is a minor convenience macro to avoid having to write the slightly awkward RestClient.globalconfig(::Val{@__MODULE__}) = expr
.
Examples
@globalconfig RequestConfig("https://api.example.com")
RestClient.@endpoint
— Macro@endpoint struct ... end
@endpoint [func(...)] -> [struct ... end] -> [payload] -> url -> resulttype
Generate an endpoint implementation from a concise shorthand form.
This macro can be applied to either an endpoint struct definition or a chain of arrow forms joined by ->
. These arrow forms represent the flow of information:
- The creation of a request, from a function call (optional)
- The
struct
used to represent the request data (optional, autogenerated from the function call) - Any payload provided with the request (if applicable)
- The page URL the request is directed to (relative to the base URL)
- The type that the response is expected to correspond to
For a basic API, this can be as simple as:
@endpoint israining(city::String) -> "checkrain/{city}" -> Bool
Using this macro is equivalent to implementing a plain struct, along with these endpoint methods:
pagename
parameters
(if needed)payload
(optionally)responsetype
In our @endpoint israining() ...
example, in addition to defining the israining
function and an (implicit) IsrainigEndpoint
struct, the macro will generate the implementations pagename(rain::IsrainingEndpoint) = "checkrain/$(rain.city)"
and responsetype(::IsrainingEndpoint) = Bool
.
In addition to applying @endpoint
to a sequence of arrow forms you can also apply it to a struct
that starts with a special line of non-struct
arrows.
For instance, our israining()
example could be alternatively written as:
@endpoint struct IsrainingEndpoint
israining(city) -> "checkrain/{city}" -> Bool
city::String
end
This generates the exact same code as the initial example form.
For more information on how each component of the shorthand form is interpreted, as well as more complex examples, see the extended help section.
See also: pagename
, parameters
, payload
, responsetype
, Request
, globalconfig
, @globalconfig
.
Extended help
Function forms
Should you chose to include a function call as the first arrow form, a function definition with a matching signature will be generated. This requires globalconfig(::Val{@__Module__})
to be defined, as is done by @globalconfig
.
Without a struct
form, the arguments of the function will be used to generate a struct
with a derived name ($(titlecase(funcname))Endpoint
), and populate its fields.
With a struct
form, all arguments that match a field name will inherit the field's type and default value . All the fields of the struct not present in the signature will be added as keyword arguments. This makes function forms an easy way to prescribe positional, mandatory arguments.
Struct forms
The endpoint struct can be given explicitly, or generated from the function call. For instance, the function call api(target::String; count::Int=1)
will generate this struct form:
@kwdef struct ApiEndpoint
target::String
count::Int = 1
end
When autogenerated, or not specified, the endpoint supertype is selected from AbstractEndpoint
, SingleEndpoint
, and ListEndpoint
based on the supertype of the output form.
If @endpoint
is directly applied to a struct
, the first line of the struct is removed and parsed as a series of arrow forms.
@endpoint ApiEndpoint
api(target; count) -> ...
target::String
count::Int = 1
end
This makes no difference to the generated code, and is purely a matter of personal preference (I prefer explicit structs for more complex forms).
Payload forms
HTTP methods like :post
, :put
, and :patch
include a content payload (also referred to as the request body). The payload can be specified with a payload form that names a field of the endpoint struct, names a global variable, or is an expression is evaluated to generate the payload value (with the current endpoint available through the anaphoric variable self
).
Unless otherwise specified, an endpoint that includes a payload is assumed to be a :post
request (:get
without).
@endpoint upload(content::String) -> content -> "create" -> Status
This is equivalent to the explicit HTTP method form:
@endpoint upload(content::String) -> content -> :post("create") -> Status
In this example content
is a String
, and so a :post
request is made to "$BASEURL/create"
with content
as the payload/body. More complex types are encoded according with writepayload
according to dataformat
.
URL forms
The endpoint URL is represent by a string, relative to the base URL of the API (a leading /
is stripped). Such paths usually consist of a path component, and optionally a query component.
"some/path/for/my/api?query=param&other=value"
Within this endpoint URL, you can include references to fields of the struct (or global variables) by surrounding them with curly braces.
"page/{somefield}?param={another}&{globalvar}=7"
Parameters with a value of nothing
are omitted from the query string.
For convenience, a parameter by the same name as the field can be referred to by the field name alone (e.g. ?{var}
instead of ?var={var}
).
In more complex cases, arbitrary Julia code can be included in the curly braces. This code will be evaluated with the endpoint value bound the the anaphoric variable self
as well as config
for the request configuration.
"page/{if self.new \"new\" else \"fetch\" end}/{id}"
If that's not enough, you can also use an arbitrary expression in place of the endpoint string:
if self.create "pages/create/" else "pages/fetch/id/" * self.id end
When using expression instead of a string though, curly braces are not interpreted as field references.
Specifying the HTTP method
By default, it is guessed whether the API expects a :get
or :post
request based on the presence or absence of an input form. This can be explicitly specified by wrapping the URL form in an HTTP method symbol, for example:
:head("existence/{entity}")
Response type forms
It is sensible to parse the raw response into a more informative Julia representation. The response type specifies what type the response can be parsed to, which is performed by interpretresponse
according to dataformat
.
Often the result type will name a struct defined with [@jsondef
].
The parsed result can be reinterpreted into another form with postprocess
. By default, ListResponse
-type responses from a ListEndpoint
are restructured into a List
to facilitate paging.
Examples
@endpoint struct ShuffleEndpoint <: SingleEndpoint
"deck/{deck}/shuffle?remaining={ifelse(self.remaining, '1', '0')}" -> Deck
deck::String
remaining::Bool
end
This is equivalent to defining the struct by itself, and then separately defining the three basic endpoint methods.
struct ShuffleEndpoint <: SingleEndpoint
deck::String
remaining::Bool
end
RestClient.pagename(shuf::ShuffleEndpoint) =
"deck/$(shuf.deck)/shuffle"
RestClient.parameters(shuf::ShuffleEndpoint) =
["remaining" => string(shuf.remaining)]
RestClient.responsetype(shuf::ShuffleEndpoint) = Deck
RestClient.@jsondef
— Macro@jsondef [kind] struct ... end
Define a struct that can be used with JSON3
.
This macro conveniently combines the following pieces:
@kwdef
to define keyword constructors for the struct.- Custom
Base.show
method to show the struct with keyword arguments, omitting default values. StructTypes.StructType
to define the struct type, andStructTypes.names
(if needed) to define the JSON field mapping.RestClient.dataformat
to declare that this struct is JSON-formatted.
Note the name."json_field"
syntax demonstrated in the examples, that allows for declaration of the JSON object key that should be mapped to the field.
Optionally the JSON representation kind
can be specified. It defaults to Struct
, but can any of: Struct
, Dict
, Array
, Vector
, String
, Number
, Bool
, Nothing
.
This macro is implemented in a package extension, and so requires JSON3
to be loaded before it can be used.
Examples
@jsondef struct DocumentStatus
exists::Bool # Required, goes by 'exists' in JSON too
status."document_status"::Union{String, Nothing} = nothing
url."document_url"::Union{String, Nothing} = nothing
age::Int = 0 # Known as 'age' in JSON too, defaults to 0
end
RestClient.@xmldef
— Macro@xmldef struct ... end
Define a struct that can be used with XML
.
This macro conveniently combines the following pieces:
- XML deserialization
@kwdef
to define keyword constructors for the struct.RestClient.dataformat
to declare that this struct is XML-formatted.
Note the name."xpath"
syntax demonstrated in the examples, that allows for declaration of the (simple) XPath that should be used to extract the field. The subset of supported XPath components are:
nodetag
to extract all immediate children with a given tag namerelative/node/paths
*
to extract all childrentext()
to extract the text content of a node@attr
to extract the value of an attributenodetag[i]
to extract thei
-th child of typenodetag
nodetag[last()]
to extract the last child of typenodetag
This macro is implemented in a package extension, and so requires XML
to be loaded before it can be used.
Examples
@xmldef struct DocumentStatus
exists."status/@exists"::Bool
status."status/text()"::String
url."status/@url"::Union{String, Nothing} = nothing
age."status/@age"::Int
end
Request types
RestClient.Request
— TypeRequest{kind, E<:AbstractEndpoint}
A request to an API endpoint, with a specific configuration.
This is the complete set of information required to make a kind
HTTP request to an endpoint E
.
See also: AbstractEndpoint
, RequestConfig
.
Data flow
╭─╴config╶────────────────────────────╮
│ ╎ │
│ ╎ ╭─▶ responsetype ╾─────┼────────────────┬──▶ dataformat ╾───╮
Request╶─┤ ╰╶╶╶╶╶╶╶╶│ │ ╰─────────╮ │
│ ├─▶ pagename ╾───╮ │ ╓┄┄*debug*┄┄╖ │ ╭──────╯
│ │ ├──▶ url ╾─┬─━─▶ request ┊ ╭─▶ interpret ╾──▶ data
├─╴endpoint╶───┼─▶ parameters ╾─╯ │ ┊ ┠────━─┤ │
│ │ ├─━──▶ cache ┊ │ ╭─────╯
│ ├─▶ parameters ╾────────────┤ ┊ ┊ ╰─────────╮ │
│ │ │ ╙┄┄┄┄┄┄┄┄┄┄┄╜ postprocess ╾──▶ result
│ *╰─▶ payload ─▶ writepayload╶╯ │
│ ╰─▶ dataformat ╾╯ │
╰─────────┬────────────────────────────────────────────────────────────╯
╰────▶ validate (before initiating the request)
* Only for POST requests ╶╶ Optional first argument
RestClient.RequestConfig
— TypeRequestConfig
The general configuration for a request to the API, not tied to any specific endpoint.
RestClient.AbstractEndpoint
— TypeAbstractEndpoint
Abstract supertype for API endpoints.
Usually you will want to subtype either SingleEndpoint
or ListEndpoint
, which share the same interface as AbstractEndpoint
but have additional semantics.
Interface
pagename([config::RequestConfig], endpoint::AbstractEndpoint) -> String
headers([config::RequestConfig], endpoint::AbstractEndpoint) -> Vector{Pair{String, String}}
parameters([config::RequestConfig], endpoint::AbstractEndpoint) -> Vector{Pair{String, String}}
responsetype(endpoint::AbstractEndpoint) -> Union{Type, Nothing}
validate([config::RequestConfig], endpoint::AbstractEndpoint) -> Bool
postprocess([response::Downloads.Response], request::Request, data) -> Any
All of these functions but pagename
have default implementations.
See also: Request
, dataformat
, interpretresponse
.
RestClient.SingleEndpoint
— TypeSingleEndpoint <: AbstractEndpoint
Abstract supertype for API endpoints that return a single value.
See also: AbstractEndpoint
, SingleResponse
, Single
.
RestClient.ListEndpoint
— TypeListEndpoint <: AbstractEndpoint
Abstract supertype for API endpoints that return a list of values.
See also: AbstractEndpoint
, ListResponse
, List
.
Response types
RestClient.SingleResponse
— TypeSingleResponse{T}
Abstract supertype for responses that contain a single T
item and (optionally) metadata.
Interface
Subtypes of SingleResponse
may need to define these two methods:
contents(single::SingleResponse{T}) -> T
metadata(single::SingleResponse{T}) -> Dict{Symbol, Any}
Both have generic implementations that are sufficient for simple cases.
RestClient.Single
— TypeSingle{T, E<:SingleEndpoint}
Holds a single value of type T
returned from an API endpoint, along with request information and metadata.
See also: SingleEndpoint
, SingleResponse
.
RestClient.ListResponse
— TypeListResponse{T}
Abstract supertype for responses that contain a list of T
items and (optionally) metadata.
Interface
Subtypes of ListResponse
may need to define these two methods:
contents(list::ListResponse{T}) -> Vector{T}
metadata(list::ListResponse{T}) -> Dict{Symbol, Any}
Both have generic implementations that are sufficient for simple cases.
RestClient.List
— TypeList{T, E<:ListEndpoint}
Holds a list of values of type T
returned from an API endpoint, along with request information and metadata.
See also: ListEndpoint
, ListResponse
.
Request interface
RestClient.globalconfig
— Functionglobalconfig(::Val{::Module}) -> RequestConfig
Return the global configuration for the given module.
This is used in @endpoint
generated API functions.
Be careful not to accidentally define this function in a way that generates a new RequestConfig
every time it is called, as this will cause state information (like rate limits) to be lost between requests.
RestClient.perform
— Functionperform(req::Request{kind}) -> Any
Validate and perform the request req
, and return the result.
The specific behaviour is determined by the kind
of the request, which corresponds to an HTTP method name (:get
, :post
, etc.).
RestClient.pagename
— Functionpagename([config::RequestConfig], endpoint::AbstractEndpoint) -> String
Return the name of the page for the given endpoint
.
This is combined with the base URL and parameters to form the full URL for the request.
Part of the AbstractEndpoint
interface.
RestClient.headers
— Functionheaders([config::RequestConfig], endpoint::AbstractEndpoint) -> Vector{Pair{String, String}}
Return headers for the given endpoint
.
The default implementation returns an empty list.
When no Content-Type
header is provided and mimetype
is defined for the format, the Content-Type
header is set to the value from mimetype
.
RestClient.parameters
— Functionparameters([config::RequestConfig], endpoint::AbstractEndpoint) -> Vector{Pair{String, String}}
Return URI parameters for the given endpoint
.
This are combined with the endpoint URL to form the full query URL.
The default implementation returns an empty list.
Part of the AbstractEndpoint
interface.
RestClient.payload
— Functionpayload([config::RequestConfig], endpoint::AbstractEndpoint) -> Any
Return the payload for the given endpoint
.
This is used for POST requests, and is sent as the body of the request.
Part of the AbstractEndpoint
interface.
Response interface
RestClient.validate
— Functionvalidate([config::RequestConfig], endpoint::AbstractEndpoint) -> Bool
Check if the request to endpoint
according to config
is valid.
This is called before the request is made, and can be used to check if the request is well-formed. This is the appropriate place to emit warnings about potential issues with the request.
Return true
if the request should proceed, false
otherwise.
The default implementation always returns true
.
Part of the AbstractEndpoint
interface.
RestClient.responsetype
— Functionresponsetype(endpoint::AbstractEndpoint) -> Type
Return the type of the response for the given endpoint.
Together with dataformat
, this is used to parse the response.
If IO
(the default implementation), the response is not parsed at all.
Part of the AbstractEndpoint
interface.
RestClient.postprocess
— Functionpostprocess([response::Downloads.Response], request::Request, data) -> Any
Post-process the data returned by the request.
There are three generic implementations provided:
- For
SingleEndpoint
requests that return aSingleResponse
, thedata
is wrapped in aSingle
object. - For
ListEndpoint
requests that return aListResponse
, thedata
are wrapped in aList
object. - For all other endpoints, the data is returned as-is.
Part of the AbstractEndpoint
interface.
RestClient.contents
— Functioncontent(response::SingleResponse{T}) -> T
Return the content of the response.
content(response::ListResponse{T}) -> Vector{T}
Return the items of the response.
RestClient.metadata
— Functionmetadata(response::SingleResponse) -> Dict{Symbol, Any}
metadata(response::ListResponse) -> Dict{Symbol, Any}
Return metadata for the given response.
The default implementation returns an empty dictionary.
RestClient.nextpage
— Functionnextpage(response::List) -> Union{List, Nothing}
Fetch the next page of results after response
.
If there are no more pages, or this method is not available for the given endpoint, return nothing
.
RestClient.thispagenumber
— Functionthispagenumber(response::List) -> Union{Int, Nothing}
Return the current page number of response
, if known.
RestClient.remainingpages
— Functionremainingpages(response::List) -> Union{Int, Nothing}
Return the number of remaining pages after response
, if known.
Content formatting
RestClient.AbstractFormat
— TypeRestClient.RawFormat
— TypeRawFormat <: AbstractFormat
Singleton type for raw response formats.
RestClient.JSONFormat
— TypeJSONFormat <: AbstractFormat
Singleton type for JSON request/response formats.
RestClient.XMLFormat
— TypeXMLFormat <: AbstractFormat
Singleton type for XML request/response formats.
RestClient.dataformat
— Functiondataformat([endpoint::AbstractEndpoint], ::Type{T}) -> AbstractFormat
Return the expected format that T
is represented by in requests to and responses from endpoint
.
Using the default dataformat(::Type)
method, the format is RawFormat
.
A dataformat(::Type)
method is automatically defined when invoking @jsondef
.
RestClient.interpretresponse
— Functioninterpretresponse(data::IO, fmt::AbstractFormat, ::Type{T}) -> value::T
Interpret data
as a response of type T
according to fmt
.
RestClient.writepayload
— Functionwritepayload(dest::IO, fmt::AbstractFormat, data)
Write data
to dest
according to fmt
.