diff --git a/Project.toml b/Project.toml index 87e15a5..608be16 100644 --- a/Project.toml +++ b/Project.toml @@ -8,6 +8,8 @@ Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" [compat] @@ -19,5 +21,6 @@ DocStringExtensions = "0.8, 0.9" # The wider compatibility range for `HTTP` ensures compatibility with # up-to-date versions of `Pluto`, etc. HTTP = "0.8, 0.9, 1" +JSON = "0.21.3" Reexport = "1.2.2" julia = "1.6" diff --git a/docs/Manifest.toml b/docs/Manifest.toml index acbac7c..6efeb7e 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -64,7 +64,7 @@ uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" version = "0.21.3" [[deps.Kroki]] -deps = ["Base64", "CodecZlib", "DocStringExtensions", "HTTP", "Reexport"] +deps = ["Base64", "CodecZlib", "DocStringExtensions", "HTTP", "JSON", "Markdown", "Reexport"] path = ".." uuid = "b3565e16-c1f2-4fe9-b4ab-221c88942068" diff --git a/src/Kroki.jl b/src/Kroki.jl index dc4dc0c..835fcca 100644 --- a/src/Kroki.jl +++ b/src/Kroki.jl @@ -18,9 +18,6 @@ include("./kroki/documentation.jl") using .Documentation @setupDocstringMarkup() -include("./kroki/service.jl") -using .Service: ENDPOINT - export Diagram, render # Convenience short-hand to make further type definitions more straightforward @@ -429,6 +426,9 @@ function Base.show(io::IO, diagram::Diagram) end end +include("./kroki/service.jl") +using .Service: ENDPOINT + include("./kroki/string_literals.jl") @reexport using .StringLiterals diff --git a/src/kroki/exceptions.jl b/src/kroki/exceptions.jl index 7b36951..7fe1901 100644 --- a/src/kroki/exceptions.jl +++ b/src/kroki/exceptions.jl @@ -39,6 +39,26 @@ function Base.showerror(io::IO, error::DiagramPathOrSpecificationError) print(io, message) end +""" +An `Exception` to be thrown when [`Kroki.Service.info`](@ref) cannot retrieve +its information from the Kroki service configured through +[`Kroki.Service.setEndpoint!`](@ref). +""" +struct InfoRetrievalError <: Exception + "The endpoint that was queried for information about a Kroki service." + endpoint::String +end + +Base.showerror(io::IO, error::InfoRetrievalError) = print( + io, + """ + The Kroki service at $(error.endpoint) could not be queried for information. + + Please verify a Kroki service is active at this address, or reconfigure using + `Kroki.Service.setEndpoint!`. + """, +) + """ An `Exception` to be thrown when a [`Diagram`](@ref) representing an invalid specification is passed to [`render`](@ref Kroki.render). diff --git a/src/kroki/service.jl b/src/kroki/service.jl index 6c0cc01..9fbd065 100644 --- a/src/kroki/service.jl +++ b/src/kroki/service.jl @@ -8,6 +8,13 @@ Compose](https://docs.docker.com/compose/) are available on the system. """ module Service +using HTTP: get as httpget +using JSON: parse as parseJSON +using Markdown: parse as parseMarkdown + +using ..Exceptions: InfoRetrievalError +using ..Kroki: toMarkdownLink + using ..Documentation @setupDocstringMarkup() @@ -84,6 +91,62 @@ executeDockerCompose(cmd::String) = executeDockerCompose([cmd]) # be mocked out in tests const EXECUTE_DOCKER_COMPOSE = Ref{Any}(executeDockerCompose) +function infoVersionOverview( + kroki_service_version::Dict{String, Any}, + diagram_type_versions::Vector{String}, +) + return parseMarkdown( + """ + The active Kroki service ($(ENDPOINT[])) runs + v$(VersionNumber(kroki_service_version["number"])) + ($(kroki_service_version["build_hash"])), which is configured with the + following diagram type versions. + + | Diagram Type | Version | + | :-- | :-- | + $(join(diagram_type_versions, '\n')) + + !!! info "Diagram type availability" + The presence of a diagram type in this list does not mean it is actually + supported by the service at $(ENDPOINT[]). This is due to some diagram + types requiring additional services that may not be available, as they need + to be managed separately. See [the architecture section on Kroki's + website](https://docs.kroki.io/kroki/architecture) for more information. + """, + ) +end + +""" +Provides an overview of the (versions of) tools supporting the different +diagram types based on information provided by the service as configured +through [`setEndpoint!`](@ref). + +# Example + +`julia> Kroki.Service.info()` + +$(info()) +""" +function info() + try + response = httpget("$(ENDPOINT[])/health") + + versions = get(parseJSON(String(response.body)), "version", nothing) + + kroki_service_version = get(versions, "kroki", nothing) + delete!(versions, "kroki") + + diagram_type_versions = sort([ + "| $(toMarkdownLink(Symbol(diagram_type))) | $(version) |" for + (diagram_type, version) in versions + ]) + + return infoVersionOverview(kroki_service_version, diagram_type_versions) + catch + throw(InfoRetrievalError(ENDPOINT[])) + end +end + """ Sets the [`ENDPOINT`](@ref) using a fallback mechanism if no `endpoint` is provided. diff --git a/test/Project.toml b/test/Project.toml index 866cdf2..a7b1909 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,3 +1,4 @@ [deps] +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" SimpleMock = "a896ed2c-15a5-4479-b61d-a0e88e2a1d25" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/kroki/exceptions_test.jl b/test/kroki/exceptions_test.jl index c4e41fc..9455458 100644 --- a/test/kroki/exceptions_test.jl +++ b/test/kroki/exceptions_test.jl @@ -5,6 +5,7 @@ using Test: @test, @test_throws, @testset using Kroki: Diagram, render using Kroki.Exceptions: DiagramPathOrSpecificationError, + InfoRetrievalError, InvalidDiagramSpecificationError, InvalidOutputFormatError, StatusError, # Imported from HTTP through Kroki @@ -63,6 +64,19 @@ end end end + @testset "`InfoRetrievalError`" begin + expected_endpoint = "http://test.endpoint" + + rendered_error = sprint(showerror, InfoRetrievalError(expected_endpoint)) + + # Should state the cause of the error in a readable fashion + @test occursin("queried for information", rendered_error) + + # Should contain the configured `ENDPOINT` and suggest to update it + @test occursin(expected_endpoint, rendered_error) + @test occursin("setEndpoint!", rendered_error) + end + @testset "`RenderError`" begin # `RenderError`s are thrown from the `render` function whenever an error # occurs. Some of these benefit from rewrites into more descriptive diff --git a/test/kroki/service_test.jl b/test/kroki/service_test.jl index 4fd96df..ab62d08 100644 --- a/test/kroki/service_test.jl +++ b/test/kroki/service_test.jl @@ -1,18 +1,21 @@ module ServiceTest +using Kroki.Exceptions: InfoRetrievalError using Kroki.Service: DEFAULT_ENDPOINT, DockerComposeExecutionError, ENDPOINT, EXECUTE_DOCKER_COMPOSE, executeDockerCompose, + info, setEndpoint!, start!, status, stop!, update! +using Markdown using SimpleMock -using Test: @testset, @test, @test_logs, @test_skip +using Test: @test, @test_logs, @test_skip, @test_throws, @testset # Helper function to temporarily replace `EXECUTE_DOCKER_COMPOSE` with a # `Mock`. Used to gain control over `docker-compose` behavior for local service @@ -73,6 +76,50 @@ end ENDPOINT[] = original_endpoint end + @testset "`info`" begin + # These are mostly a collection of spot checks, to make sure the report + # contains at least some useful information + @testset "with a valid Kroki service" begin + info_report = info() + + # For the best visualization in different contexts, the report should be + # created as Markdown + @test info_report isa Markdown.MD + + # All other tests are more easily performed against the rendered Markdown + rendered_info_report = "$(info_report)" + + # The report should include the address of the active Kroki service + @test occursin("active Kroki service ($(ENDPOINT[]))", rendered_info_report) + + # The report should contain an overview of diagram types with versions, + # with proper headers + @test occursin(r"| Diagram Type\s+| Version\s+|", rendered_info_report) + + # The report should include the friendly names of diagram types and links + # to their websites + @test occursin("[PlantUML](https://plantuml.com)", rendered_info_report) + @test occursin("[Structurizr](https://structurizr.com)", rendered_info_report) + + # Kroki itself should not show up within the diagram types table + @test !occursin("[kroki]", rendered_info_report) + + # It should include a note no + @test occursin("!!! info \"Diagram type availability\"", rendered_info_report) + end + + @testset "without a valid Kroki service" begin + original_endpoint = ENDPOINT[] + setEndpoint!("http://does.not.exist") + + try + @test_throws(InfoRetrievalError, info()) + finally + setEndpoint!(original_endpoint) + end + end + end + @testset "local instance management" begin @static if Sys.which("docker-compose") !== nothing @testset "wraps `docker-compose` with local service definitions" begin