Skip to content

Commit

Permalink
feature/more-autodoc-improvements (#206)
Browse files Browse the repository at this point in the history
* Fixed content schema ordering (now respects insertion order)
* Added support for default values in components and parameters
* Improved how the function / type name is passed in splitdef()
  • Loading branch information
ndortega authored Jun 19, 2024
1 parent a02b131 commit 5658fcf
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 19 deletions.
73 changes: 73 additions & 0 deletions demo/bankingdemoapp.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
module BankingAppDemo
using Base: @kwdef
using Oxygen

struct Address
street::String
city::String
state::String
zip_code::String
country::String
end

struct User
id::Int
first_name::String
last_name::String
email::String
address::Address
end

@kwdef struct BankAccount
id::Int
account_number::String
account_type::String = "checking"
balance::Float64
user::User
end


@get "/" function()
return "Welcome to the Banking App Demo"
end

"""
Setup User related routes
"""

user = router("/user", tags=["user"])

@post user("/json") function(req, data::Json{User})
return data.payload
end

@post user("/form") function(req, data::Form{User})
return data.payload
end

@post user("/headers") function(req, data::Header{User})
return data.payload
end


"""
Setup Account related routes
"""

acct = router("/account", tags=["account"])

@post acct("/json") function(req, data::Json{BankAccount})
return data.payload
end

@post acct("/form") function(req, data::Form{BankAccount})
return data.payload
end

@post acct("/headers") function(req, data::Header{BankAccount})
return data.payload
end

serve()

end
56 changes: 43 additions & 13 deletions src/autodoc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,44 @@ function getformat(type::Type) :: Nullable{String}
return nothing
end

function getcomponent(name::AbstractString) :: String
return "#/components/schemas/$name"
end

function getcomponent(t::DataType) :: String
return getcomponent(string(nameof(t)))
end

function createparam(p::Param{T}, paramtype::String) :: Dict where {T}
param = Dict(
"in" => paramtype, # path, query, header (where the parameter is located)
"name" => String(p.name),
"required" => paramtype == "path" ? true : isrequired(p), # path params are always required
"schema" => Dict(
"type" => gettype(p.type)
)
)

schema = Dict("type" => gettype(p.type))

# Add ref if the type is a custom struct
if schema["type"] == "object"
schema["\$ref"] = getcomponent(p.type)
end

# Add optional format if it's relevant
format = getformat(p.type)
if !isnothing(format)
param["schema"]["format"] = format
schema["format"] = format
end

# Add default value if it exists
if p.hasdefault
schema["default"] = string(p.default)
end

# path params are always required
param_required = paramtype == "path" ? true : isrequired(p)

param = Dict(
"in" => paramtype, # path, query, header (where the parameter is located)
"name" => String(p.name),
"required" => param_required,
"schema" => schema
)

return param
end

Expand Down Expand Up @@ -123,7 +148,7 @@ The only exception to this is the text/plain case, which excepts the Body extrac
If there are more than one Body extractor, the type defaults to string - since this is
the only way to represent multiple formats at the same time.
"""
function formatcontent(bodyparams::Vector) :: Dict
function formatcontent(bodyparams::Vector) :: OrderedDict

body_refs = Dict{String,Vector{String}}()
body_types = Dict()
Expand All @@ -143,7 +168,7 @@ function formatcontent(bodyparams::Vector) :: Dict
body_refs[extractor_name] = []
end

body_refs[extractor_name] = vcat(body_refs[extractor_name], "#/components/schemas/$inner_type_name")
body_refs[extractor_name] = vcat(body_refs[extractor_name], getcomponent(inner_type_name))
end

jsonschema = collectschemarefs(body_refs, ["Json", "JsonFragment"])
Expand Down Expand Up @@ -297,7 +322,7 @@ end


function is_custom_struct(T::Type)
return isstructtype(T) && T.name.module (Base, Core)
return T.name.module (Base, Core) && (isstructtype(T) || isabstracttype(T))
end

# takes a struct and converts it into an openapi 3.0 compliant dictionary
Expand Down Expand Up @@ -326,7 +351,7 @@ function convertobject!(type::Type, schemas::Dict) :: Dict
# Case 1: Recursively convert nested structs & register schemas
if is_custom_struct(current_type) && !haskey(schemas, current_name)
# Set the field to be a reference to the custom struct
obj["properties"][field_name] = Dict("\$ref" => "#/components/schemas/$current_name")
obj["properties"][field_name] = Dict("\$ref" => getcomponent(current_name))
# Recursively convert nested structs
convertobject!(current_type, schemas)

Expand All @@ -340,6 +365,11 @@ function convertobject!(type::Type, schemas::Dict) :: Dict
current_field["format"] = format
end

# Add default value if it exists
if p.hasdefault
current_field["default"] = string(p.default)
end

# convert the current field
obj["properties"][field_name] = current_field
end
Expand Down
11 changes: 5 additions & 6 deletions src/reflection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,9 @@ end
Used to extract the function signature from regular Julia functions.
"""
function splitdef(f::Function; start=1)
return splitdef(Base.code_lowered(f), methods(f), start=start)
method_defs = methods(f)
func_name = first(method_defs).name
return splitdef(Base.code_lowered(f), methods(f), func_name, start=start)
end


Expand All @@ -326,7 +328,7 @@ for structs to have multiple constructors with the same parameter names as both
keyword args and regular args.
"""
function splitdef(t::DataType; start=1)
results = splitdef(Base.code_lowered(t), methods(t), start=start)
results = splitdef(Base.code_lowered(t), methods(t), nameof(t), start=start)
sig_map = Dict{Symbol,Param}()
for param in results.sig
# merge parameters with the same name
Expand All @@ -342,10 +344,7 @@ function splitdef(t::DataType; start=1)
end


function splitdef(info::Vector{Core.CodeInfo}, method_defs::Base.MethodList; start=1)

# Get the function name from the first constructor
func_name = first(method_defs).name
function splitdef(info::Vector{Core.CodeInfo}, method_defs::Base.MethodList, func_name::Symbol; start=1)

# Extract parameter names and types
param_names, param_types, kwarg_names = getsignames(method_defs)
Expand Down

0 comments on commit 5658fcf

Please sign in to comment.