diff --git a/src/const.jl b/src/const.jl index d4be014..cae867c 100644 --- a/src/const.jl +++ b/src/const.jl @@ -6,7 +6,7 @@ module Services end "Shell command execution request message." -@msg "org.arl.fjage.shell.ShellExecReq" struct ShellExecReq +@message "org.arl.fjage.shell.ShellExecReq" struct ShellExecReq cmd::Union{String,Nothing} = nothing script::Union{String,Nothing} = nothing args::Vector{String} = String[] diff --git a/src/msg.jl b/src/msg.jl index 01bdd91..1954d5d 100644 --- a/src/msg.jl +++ b/src/msg.jl @@ -1,4 +1,4 @@ -export Performative, Message, GenericMessage, MessageClass, AbstractMessageClass, clone, ParameterReq, ParameterRsp, set! +export Performative, Message, GenericMessage, @message, classname, clone, ParameterReq, ParameterRsp, set! # global variables const _messageclasses = Dict{String,DataType}() @@ -22,132 +22,84 @@ end "Base class for messages transmitted by one agent to another." abstract type Message end -function clazz end +""" + classname(msg::Message) + +Return the fully qualified class name of a message. +""" +function classname end -function _msg(clazz, perf, sdef) +function _message(classname, perf, sdef) if @capture(sdef, struct T_ <: P_ fields__ end) - push!(fields, :(msgID::String = string(Fjage.uuid4()))) - push!(fields, :(perf::Symbol = $perf)) + if T == P + T = Symbol("_" * string(T)) + extra = :( $(P)(; kwargs...) = $(T)(; kwargs...) ) + else + extra = :() + end + push!(fields, :(messageID::String = string(Fjage.uuid4()))) + push!(fields, :(performative::Symbol = $perf)) push!(fields, :(sender::Union{AgentID,Nothing} = nothing)) push!(fields, :(recipient::Union{AgentID,Nothing} = nothing)) push!(fields, :(inReplyTo::Union{String,Nothing} = nothing)) push!(fields, :(sentAt::Int64 = 0)) quote Base.@kwdef mutable struct $(T) <: $(P); $(fields...); end - Fjage.clazz(::Type{$(T)}) = $(clazz) - Fjage.clazz(::$(T)) = $(clazz) - Fjage._messageclasses[$(clazz)] = $(T) + Fjage.classname(::Type{$(T)}) = $(classname) + Fjage.classname(::$(T)) = $(classname) + Fjage._messageclasses[$(classname)] = $(T) + $extra end |> esc elseif @capture(sdef, struct T_ fields__ end) - push!(fields, :(msgID::String = string(Fjage.uuid4()))) - push!(fields, :(perf::Symbol = $perf)) + push!(fields, :(messageID::String = string(Fjage.uuid4()))) + push!(fields, :(performative::Symbol = $perf)) push!(fields, :(sender::Union{AgentID,Nothing} = nothing)) push!(fields, :(recipient::Union{AgentID,Nothing} = nothing)) push!(fields, :(inReplyTo::Union{String,Nothing} = nothing)) push!(fields, :(sentAt::Int64 = 0)) quote Base.@kwdef mutable struct $(T) <: Fjage.Message; $(fields...); end - Fjage.clazz(::Type{$(T)}) = $(clazz) - Fjage.clazz(::$(T)) = $(clazz) - Fjage._messageclasses[$(clazz)] = $(T) + Fjage.classname(::Type{$(T)}) = $(classname) + Fjage.classname(::$(T)) = $(classname) + Fjage._messageclasses[$(classname)] = $(T) end |> esc else @error "Bad message definition" end end -macro msg(clazz, perf, sdef) +""" + @message classname [performative] struct mtype [<: supertype] + fields... + end + +Create a message class from a fully qualified class name. If a performative is not +specified, it is guessed based on the class name. For class names ending with "Req", +the performative is assumed to be REQUEST, and for all other messages, INFORM. + +# Examples + +```julia-repl +julia> @message "org.arl.fjage.shell.MyShellExecReq" struct MyShellExecReq + cmd::String + end +julia> req = MyShellExecReq(cmd="ps") +MyShellExecReq:REQUEST[cmd:"ps"] +``` +""" +macro message(classname, perf, sdef) perf = QuoteNode(eval(perf)) - _msg(clazz, perf, sdef) + _message(classname, perf, sdef) end -macro msg(clazz, sdef) - perf = endswith(clazz, "Req") ? :(:REQUEST) : :(:INFORM) - _msg(clazz, perf, sdef) +macro message(classname, sdef) + perf = endswith(classname, "Req") ? :(:REQUEST) : :(:INFORM) + _message(classname, perf, sdef) end - -# """ -# mtype = MessageClass(context, clazz[, superclass[, performative]]) - -# Create a message class from a fully qualified class name. If a performative is not -# specified, it is guessed based on the class name. For class names ending with "Req", -# the performative is assumed to be REQUEST, and for all other messages, INFORM. - -# # Examples - -# ```julia-repl -# julia> MyShellExecReq = MessageClass(@__MODULE__, "org.arl.fjage.shell.ShellExecReq"); -# julia> req = MyShellExecReq(cmd="ps") -# ShellExecReq: REQUEST [cmd:"ps"] -# ``` -# """ -# function MessageClass(context, clazz::String, superclass=nothing, performative=nothing) -# sname = replace(string(clazz), "." => "_") -# tname = sname -# if performative === nothing -# performative = match(r"Req$",string(clazz))==nothing ? Performative.INFORM : Performative.REQUEST -# end -# if superclass === nothing -# superclass = "$(@__MODULE__).Message" -# else -# scname = string(superclass) -# ndx = findlast(isequal('.'), scname) -# if ndx !== nothing -# scname = scname[ndx+1:end] -# end -# if scname == tname -# tname = "$(scname)_" -# end -# end -# expr = Expr(:toplevel) -# expr.args = [Meta.parse(""" -# struct $(tname) <: $(superclass) -# clazz::String -# data::Dict{String,Any} -# $(tname)(c::String, d::Dict{String,Any}) = new(c, d) -# end -# """), -# Meta.parse(""" -# function $(sname)(d::Dict{String, Any}) -# get!(d, "msgID", string($(@__MODULE__).uuid4())) -# get!(d, "perf", "$performative") -# return $(tname)("$(clazz)", d) -# end -# """), -# Meta.parse(""" -# function $(sname)(; kwargs...) -# dict = Dict{String,Any}( -# "msgID" => string($(@__MODULE__).uuid4()), -# "perf" => "$(performative)" -# ) -# for k in keys(kwargs) -# dict[string(k)] = kwargs[k] -# end -# return $(tname)("$(clazz)", dict) -# end -# """), -# Meta.parse(""" -# $(@__MODULE__)._messageclasses["$(clazz)"] = $(tname) -# """)] -# if sname != tname -# push!(expr.args, Meta.parse("$(tname)(; kwargs...) = $(sname)(; kwargs...)")) -# end -# return context.eval(expr) -# end - -# function AbstractMessageClass(context, clazz::String, performative=nothing) -# sname = replace(string(clazz), "." => "_") -# expr = Expr(:toplevel) -# expr.args = [Meta.parse("abstract type $sname <: $(@__MODULE__).Message end"), Meta.parse("$sname")] -# rv = context.eval(expr) -# MessageClass(context, clazz, rv, performative) -# return rv -# end - function clone(original::Message) cloned = deepcopy(original) - cloned.msgID = string(uuid4()) + cloned.messageID = string(uuid4()) return cloned end @@ -156,7 +108,7 @@ end registermessages(messageclasses) Register message classes with Fjage. Usually message classes are automatically registered on -creation with `@msg`. However, when developing packages, if `@msg` is used at the module level, +creation with `@message`. However, when developing packages, if `@message` is used at the module level, the types may be precompiled and the code to register the classes may not get executed at runtime. In such cases, you may need to explicitly call `registermessages()` in the `__init()__` function for the module. @@ -164,14 +116,14 @@ for the module. function registermessages(msg=subtypes(Message)) for T ∈ msg endswith(string(T), '_') && continue - s = clazz(T) + s = classname(T) _messageclasses[s] = T registermessages(subtypes(T)) end end -function _messageclass_lookup(clazz::String) - haskey(_messageclasses, clazz) && return _messageclasses[clazz] +function _messageclass_lookup(classname::String) + haskey(_messageclasses, classname) && return _messageclasses[classname] Message end @@ -181,7 +133,7 @@ function _matches(filt, msg) if typeof(filt) == DataType return typeof(msg) <: filt elseif typeof(filt) <: Message - return msg.inReplyTo == filt.msgID + return msg.inReplyTo == filt.messageID elseif typeof(filt) <: Function return filt(msg) end @@ -189,22 +141,13 @@ function _matches(filt, msg) end # adds notation message.field + function Base.getproperty(s::Message, p::Symbol) - if p == :performative - p = :perf - elseif p == :messageID - p = :msgID - end hasfield(typeof(s), p) || return nothing getfield(s, p) end function Base.setproperty!(s::Message, p::Symbol, v) - if p == :performative - p = :perf - elseif p == :messageID - p = :msgID - end hasfield(typeof(s), p) || throw(ArgumentError("$(typeof(s)) has no property called $p")) setfield!(s, p, v) end @@ -235,250 +178,305 @@ function Base.iterate(s::Message, state) (state[1][1] => state[2][1], (state[1][2:end], state[2][2:end])) end -# # pretty prints arrays without type names -# function _repr(x) -# x = repr(x) -# m = match(r"[A-Za-z0-9]+(\[.+\])", x) -# m !== nothing && (x = m[1]) -# x -# end +# pretty prints arrays without type names +function _repr(x) + x = repr(x) + m = match(r"[A-Za-z0-9]+(\[.+\])", x) + m !== nothing && (x = m[1]) + m = match(r"^\w+(\[.*)$", x) + m !== nothing && (x = m[1]) + x +end # pretty printing of messages -# function Base.show(io::IO, msg::Message) -# ndx = findlast(".", msg.__clazz__) -# s = ndx === nothing ? msg.__clazz__ : msg.__clazz__[ndx[1]+1:end] -# p = "" -# data_suffix = "" -# signal_suffix = "" -# suffix = "" -# data = msg.__data__ -# for k in keys(data) -# x = data[k] -# if k == "perf" -# s *= ": " * x -# elseif k == "data" -# if typeof(x) <: Array -# data_suffix *= "($(length(x)) bytes)" -# else -# p *= " $k:" * _repr(data[k]) -# end -# elseif k == "signal" -# if typeof(x) <: Array -# signal_suffix *= "($(length(x)) samples)" -# else -# p *= " $k:" * _repr(data[k]) -# end -# elseif k != "sender" && k != "recipient" && k != "msgID" && k != "inReplyTo" && k != "sentAt" -# if typeof(x) <: Number || typeof(x) == String || typeof(x) <: Array || typeof(x) == Bool -# p *= " $k:" * _repr(x) -# else -# suffix = "..." -# end -# end -# end -# length(suffix) > 0 && (p *= " " * suffix) -# length(signal_suffix) > 0 && (p *= " " * signal_suffix) -# length(data_suffix) > 0 && (p *= " " * data_suffix) -# p = strip(p) -# length(p) > 0 && (s *= " [$p]") -# if msg.__clazz__ == "org.arl.fjage.GenericMessage" -# m = match(r"^GenericMessage: (.*)$", s) -# m === nothing || (s = m[1]) -# end -# print(io, s) -# end +function Base.show(io::IO, msg::Message) + s = classname(msg) + ndx = findlast(".", s) + ndx === nothing || (s = s[ndx[1]+1:end]) + p = "" + data_suffix = "" + signal_suffix = "" + suffix = "" + for k in keys(msg) + x = msg[k] + if k == :performative + s *= ":" * string(x) + elseif k == :data + if typeof(x) <: Array + data_suffix *= "($(length(x)) bytes)" + else + p *= " $k:" * _repr(msg[k]) + end + elseif k == :signal + if typeof(x) <: Array + signal_suffix *= "($(length(x)) samples)" + else + p *= " $k:" * _repr(msg[k]) + end + elseif k != :sender && k != :recipient && k != :messageID && k != :inReplyTo && k != :sentAt + if typeof(x) <: Number || typeof(x) == String || typeof(x) <: Array || typeof(x) == Bool + p *= " $k:" * _repr(x) + elseif x !== nothing && x !== missing + suffix = "..." + end + end + end + length(suffix) > 0 && (p *= " " * suffix) + length(signal_suffix) > 0 && (p *= " " * signal_suffix) + length(data_suffix) > 0 && (p *= " " * data_suffix) + p = strip(p) + length(p) > 0 && (s *= "[$p]") + print(io, s) +end # concrete message without data -@msg "org.arl.fjage.Message" struct _Message end +@message "org.arl.fjage.Message" struct _Message end "Generic message type that can carry arbitrary name-value pairs as data." abstract type GenericMessage <: Message end # concrete generic message -@msg "org.arl.fjage.GenericMessage" struct _GenericMessage <: GenericMessage - data::Dict{String,Any} = Dict{String,Any}() +@message "org.arl.fjage.GenericMessage" struct _GenericMessage <: GenericMessage + __data__::Dict{Symbol,Any} = Dict{Symbol,Any}() +end + +GenericMessage(perf::Symbol=Performative.INFORM; kwargs...) = _GenericMessage(performative=perf, kwargs...) +GenericMessage(inreplyto::Message, perf::Symbol=Performative.INFORM; kwargs...) = _GenericMessage(performative=perf, inReplyTo=inreplyto.messageID, recipient=inreplyto.sender, kwargs...) + +# adds notation message.field + +function Base.getproperty(s::GenericMessage, p::Symbol) + hasfield(typeof(s), p) && return getfield(s, p) + haskey(s.__data__, p) && return s.__data__[p] + nothing +end + +function Base.setproperty!(s::GenericMessage, p::Symbol, v) + if hasfield(typeof(s), p) + setfield!(s, p, v) + else + s.__data__[p] = v + end end -GenericMessage(perf::Symbol=Performative.INFORM; kwargs...) = _GenericMessage(perf=perf, kwargs...) -GenericMessage(inreplyto::Message, perf::Symbol=Performative.INFORM; kwargs...) = _GenericMessage(perf=perf, inReplyTo=inreplyto.msgID, recipient=inreplyto.sender, kwargs...) +# dictionary interface for GenericMessages + +function Base.get(s::GenericMessage, p::Symbol, default) + hasfield(typeof(s), p) && return getfield(s, p) + haskey(s.__data__, p) && return s.__data__[p] + default +end + +function Base.keys(s::GenericMessage) + k = Set(keys(s.__data__)) + for f ∈ fieldnames(typeof(s)) + f == :__data__ && continue + push!(k, f) + end + k +end + +function Base.values(s::GenericMessage) + v = Any[] + for k ∈ keys(s) + push!(v, s[k]) + end + v +end + +Base.length(s::GenericMessage) = fieldcount(typeof(s)) - 1 + length(s.__data__) + +function Base.iterate(s::GenericMessage) + f = keys(s) + v = values(s) + (f[1] => v[1], (f[2:end], v[2:end])) +end + +""" + msg = Message([perf]) + msg = Message(inreplyto[, perf]) + +Create a message with just a performative (`perf`) and no data. If the performative +is not specified, it defaults to INFORM. If the inreplyto is specified, the message +`inReplyTo` and `recipient` fields are set accordingly. +""" +Message(perf::Symbol=Performative.INFORM) = _Message(performative=perf) +Message(inreplyto::Message, perf::Symbol=Performative.INFORM) = _Message(performative=perf, inReplyTo=inreplyto.messageID, recipient=inreplyto.sender) "Parameter request message." -@msg "org.arl.fjage.param.ParameterReq" struct ParameterReq +@message "org.arl.fjage.param.ParameterReq" struct ParameterReq index::Int = -1 param::Union{String,Nothing} = nothing value::Union{Any,Nothing} = nothing - requests::Union{Dict{String,Any},Nothing} = nothing + requests::Union{Vector{Dict{String,Any}},Nothing} = nothing end "Parameter response message." -@msg "org.arl.fjage.param.ParameterRsp" struct ParameterRsp +@message "org.arl.fjage.param.ParameterRsp" struct ParameterRsp index::Int = -1 param::Union{String,Nothing} = nothing value::Union{Any,Nothing} = nothing - values::Union{Dict{String,Any},Nothing} = nothing + values::Union{Vector{Dict{String,Any}},Nothing} = nothing +end + +# convenience methods and pretty printing for parameters + +function ParameterReq(vals...; index=-1) + req = ParameterReq(index=index) + qlist = Pair{String,Any}[] + for v ∈ vals + if v isa String + push!(qlist, Pair{String,Any}(v, nothing)) + elseif v isa Symbol + push!(qlist, Pair{String,Any}(string(v), nothing)) + elseif v isa Pair + push!(qlist, Pair{String,Any}(string(v[1]), v[2])) + end + end + if length(qlist) > 0 + q = popfirst!(qlist) + req.param = q[1] + req.value = q[2] + if length(qlist) > 0 + req.requests = Dict{String,Any}[] + for q ∈ qlist + push!(req.requests, Dict{String,Any}("param" => q[1], "value" => q[2])) + end + end + end + req end """ - msg = Message([perf]) - msg = Message(inreplyto[, perf]) + get!(p::ParameterReq, param) -Create a message with just a performative (`perf`) and no data. If the performative -is not specified, it defaults to INFORM. If the inreplyto is specified, the message -`inReplyTo` and `recipient` fields are set accordingly. +Request parameter `param` to be fetched. + +# Examples + +```julia-repl +julia> p = ParameterReq(index=1) +ParameterReq[index=1] +julia> get!(p, "modulation") +ParameterReq[index=1 modulation=?] +julia> get!(p, "fec") +ParameterReq[index=1 modulation=? ...] +``` +""" +function Base.get!(p::ParameterReq, param) + param = string(param) + if p.param === nothing + p.param = param + else + p.requests === nothing && (p.requests = Dict{String,Any}[]) + push!(p.requests, Dict{String,Any}("param" => param)) + end + p +end + +""" + set!(p::ParameterReq, param, value) + +Request parameter `param` to be set to `value`. + +# Examples + +```julia-repl +julia> p = ParameterReq(index=1) +ParameterReq[index=1] +julia> set!(p, "modulation", "ofdm") +ParameterReq[index=1 modulation=ofdm] +julia> set!(p, "fec", 1) +ParameterReq[index=1 modulation=ofdm ...] +``` """ -Message(perf::Symbol=Performative.INFORM) = _Message(perf=perf) -Message(inreplyto::Message, perf::Symbol=Performative.INFORM) = _Message(perf=perf, inReplyTo=inreplyto.msgID, recipient=inreplyto.sender) - -# # convenience methods and pretty printing for parameters - -# function org_arl_fjage_param_ParameterReq(vals...; index=-1) -# req = ParameterReq(index=index) -# qlist = Pair{String,Any}[] -# for v ∈ vals -# if v isa String -# push!(qlist, Pair{String,Any}(v, nothing)) -# elseif v isa Symbol -# push!(qlist, Pair{String,Any}(string(v), nothing)) -# elseif v isa Pair -# push!(qlist, Pair{String,Any}(string(v[1]), v[2])) -# end -# end -# if length(qlist) > 0 -# q = popfirst!(qlist) -# req.param = q[1] -# req.value = q[2] -# if length(qlist) > 0 -# req.requests = Dict{String,Any}[] -# for q ∈ qlist -# push!(req.requests, Dict{String,Any}("param" => q[1], "value" => q[2])) -# end -# end -# end -# req -# end - -# """ -# get!(p::ParameterReq, param) - -# Request parameter `param` to be fetched. - -# # Examples - -# ```julia-repl -# julia> p = ParameterReq(index=1) -# ParameterReq[index=1] -# julia> get!(p, "modulation") -# ParameterReq[index=1 modulation=?] -# julia> get!(p, "fec") -# ParameterReq[index=1 modulation=? ...] -# ``` -# """ -# function Base.get!(p::ParameterReq, param) -# if p.param === nothing -# p.param = param -# else -# p.requests === nothing && (p.requests = Dict{String,Any}[]) -# push!(p.requests, Dict{String,Any}("param" => param)) -# end -# p -# end - -# """ -# set!(p::ParameterReq, param, value) - -# Request parameter `param` to be set to `value`. - -# # Examples - -# ```julia-repl -# julia> p = ParameterReq(index=1) -# ParameterReq[index=1] -# julia> set!(p, "modulation", "ofdm") -# ParameterReq[index=1 modulation=ofdm] -# julia> set!(p, "fec", 1) -# ParameterReq[index=1 modulation=ofdm ...] -# ``` -# """ -# function set!(p::ParameterReq, param, value) -# if p.param === nothing -# p.param = param -# p.value = value -# else -# p.requests === nothing && (p.requests = Dict{String,Any}[]) -# push!(p.requests, Dict{String,Any}("param" => param, "value" => value)) -# end -# p -# end - -# """ -# get(p::ParameterRsp, param) - -# Extract parameter `param` from a parameter response message. -# """ -# function Base.get(p::ParameterRsp, key) -# skey = string(key) -# dskey = "." * skey -# (!isnothing(p.param) && (p.param == skey || endswith(p.param, dskey))) && return p.value -# vals = p.values -# if vals !== nothing -# for (k, v) ∈ vals -# (k == skey || endswith(k, dskey)) && return v -# end -# end -# nothing -# end - -# function Base.show(io::IO, p::ParameterReq) -# print(io, "ParameterReq[") -# p.index !== nothing && p.index ≥ 0 && print(io, "index=", p.index, ' ') -# p.param !== nothing && print(io, p.param, '=', (p.value === nothing ? "?" : string(p.value))) -# p.requests === nothing || print(io, " ...") -# print(io, ']') -# end - -# function Base.show(io::IO, p::ParameterRsp) -# print(io, "ParameterRsp[") -# p.index !== nothing && p.index ≥ 0 && print(io, "index=", p.index, ' ') -# p.param !== nothing && print(io, p.param, '=', p.value) -# p.values === nothing || print(io, " ...") -# print(io, ']') -# end - -# function Base.println(io::IO, p::ParameterRsp) -# plist = Pair{String,Any}[] -# if p.param !== nothing -# x = p.param -# occursin(".", x) || (x = "." * x) -# push!(plist, x => p.value) -# vs = p.values -# if vs !== nothing -# for v ∈ vs -# x = v[1] -# occursin(".", x) || (x = "." * x) -# push!(plist, x => v[2]) -# end -# end -# end -# sort!(plist; by=(x -> x[1])) -# let n = findfirst(x -> x[1] == ".title", plist) -# n === nothing || println(io, "« ", plist[n][2], " »\n") -# end -# let n = findfirst(x -> x[1] == ".description", plist) -# n === nothing || plist[n][2] == "" || println(io, plist[n][2], "\n") -# end -# prefix = "" -# ro = p.readonly === nothing ? String[] : p.readonly -# for (k, v) ∈ plist -# k === ".type" && continue -# k === ".title" && continue -# k === ".description" && continue -# ks = split(k, '.') -# cprefix = join(ks[1:end-1], '.') -# if cprefix != prefix -# prefix != "" && println(io) -# prefix = cprefix -# println(io, '[', cprefix, ']') -# end -# println(io, " ", ks[end], k ∈ ro ? " ⤇ " : " = ", v) -# end -# end +function set!(p::ParameterReq, param, value) + param = string(param) + if p.param === nothing + p.param = param + p.value = value + else + p.requests === nothing && (p.requests = Dict{String,Any}[]) + push!(p.requests, Dict{String,Any}("param" => param, "value" => value)) + end + p +end + +""" + get(p::ParameterRsp, param) + +Extract parameter `param` from a parameter response message. +""" +function Base.get(p::ParameterRsp, key) + skey = string(key) + dskey = "." * skey + (!isnothing(p.param) && (p.param == skey || endswith(p.param, dskey))) && return p.value + vals = p.values + if vals !== nothing + for (k, v) ∈ vals + (k == skey || endswith(k, dskey)) && return v + end + end + nothing +end + +function Base.show(io::IO, p::ParameterReq) + print(io, "ParameterReq[") + if p.index !== nothing && p.index ≥ 0 + print(io, "index=", p.index) + p.param === nothing || print(io, ' ') + end + p.param === nothing || print(io, p.param, '=', (p.value === nothing ? "?" : string(p.value))) + p.requests === nothing || print(io, " ...") + print(io, ']') +end + +function Base.show(io::IO, p::ParameterRsp) + print(io, "ParameterRsp[") + if p.index !== nothing && p.index ≥ 0 + print(io, "index=", p.index) + p.param === nothing || print(io, ' ') + end + p.param === nothing || print(io, p.param, '=', p.value) + p.values === nothing || print(io, " ...") + print(io, ']') +end + +function Base.println(io::IO, p::ParameterRsp) + plist = Pair{String,Any}[] + if p.param !== nothing + x = p.param + occursin(".", x) || (x = "." * x) + push!(plist, x => p.value) + vs = p.values + if vs !== nothing + for v ∈ vs + x = v[1] + occursin(".", x) || (x = "." * x) + push!(plist, x => v[2]) + end + end + end + sort!(plist; by=(x -> x[1])) + let n = findfirst(x -> x[1] == ".title", plist) + n === nothing || println(io, "« ", plist[n][2], " »\n") + end + let n = findfirst(x -> x[1] == ".description", plist) + n === nothing || plist[n][2] == "" || println(io, plist[n][2], "\n") + end + prefix = "" + ro = p.readonly === nothing ? String[] : p.readonly + for (k, v) ∈ plist + k === ".type" && continue + k === ".title" && continue + k === ".description" && continue + ks = split(k, '.') + cprefix = join(ks[1:end-1], '.') + if cprefix != prefix + prefix != "" && println(io) + prefix = cprefix + println(io, '[', cprefix, ']') + end + println(io, " ", ks[end], k ∈ ro ? " ⤇ " : " = ", v) + end +end