From 58a880ed57caace8c7a8b5bd50e7c7a207eb0312 Mon Sep 17 00:00:00 2001 From: Joris Kraak Date: Mon, 11 Jul 2022 23:14:20 +0200 Subject: [PATCH 1/2] feat(Diagram): enable loading specifications from files This is particularly useful for bigger diagrams that may be primarily maintained through different tooling, e.g. Structurizr. Or if a specific diagram is shared across different pieces of documentation. --- docs/src/api.md | 1 + src/Kroki.jl | 76 +++++++++++++++++++++++++++++++ test/assets/plantuml-example.puml | 6 +++ test/runtests.jl | 47 +++++++++++++++++++ 4 files changed, 130 insertions(+) create mode 100644 test/assets/plantuml-example.puml diff --git a/docs/src/api.md b/docs/src/api.md index 10ac66f..fb25f21 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -30,6 +30,7 @@ Filter = m -> endswith("$m", "_str") ## Private ```@docs +DiagramPathOrSpecificationError InvalidDiagramSpecificationError InvalidOutputFormatError LIMITED_DIAGRAM_SUPPORT diff --git a/src/Kroki.jl b/src/Kroki.jl index b41e21d..ea78873 100644 --- a/src/Kroki.jl +++ b/src/Kroki.jl @@ -23,9 +23,31 @@ using .Service: ENDPOINT export Diagram, render +# Convenience short-hand to make further type definitions more straightforward +# to write +const Maybe{T} = Union{Nothing, T} where {T} + """ A representation of a diagram that can be rendered by a Kroki service. +# Constructors + +``` +Diagram(type::Symbol, specification::AbstractString) +``` + +Constructs a `Diagram` from the `specification` for a specific `type` of +diagram. + +``` +Diagram(type::Symbol; path::AbstractString, specification::AbstractString) +``` + +Constructs a `Diagram` from the `specification` for a specific `type` of +diagram, or loads the `specification` from the provided `path`. + +Specifying both, or neither, keyword arguments is invalid. + # Examples ``` @@ -50,9 +72,63 @@ struct Diagram """ type::Symbol + """ + Constructs a [`Diagram`](@ref) from the `specification` for a specific `type` + of diagram. + """ Diagram(type::Symbol, specification::AbstractString) = new(specification, type) end +""" +Constructs a [`Diagram`](@ref) from the `specification` for a specific `type` +of diagram, or loads the `specification` from the provided `path`. + +Specifying both, or neither, keyword arguments is invalid. +""" +function Diagram( + type::Symbol; + path::Maybe{AbstractString} = nothing, + specification::Maybe{AbstractString} = nothing, +) + path_provided = !isnothing(path) + specification_provided = !isnothing(specification) + + if path_provided && specification_provided + throw(DiagramPathOrSpecificationError(path, specification)) + elseif !path_provided && !specification_provided + throw(DiagramPathOrSpecificationError(path, specification)) + elseif path_provided + Diagram(type, read(path, String)) + else + Diagram(type, specification) + end +end + +""" +An `Exception` to be thrown when the `path` and `specification` keyword +arguments to [`Diagram`](@ref) are not specified mutually exclusive. +""" +struct DiagramPathOrSpecificationError <: Exception + path::Maybe{AbstractString} + specification::Maybe{AbstractString} +end + +function Base.showerror(io::IO, error::DiagramPathOrSpecificationError) + not_specified = "" + + path_description = isnothing(error.path) ? not_specified : error.path + specification_description = + isnothing(error.specification) ? not_specified : error.specification + + message = """ + Either `path` or `specification` should be specified: + * `path`: '$(path_description)' + * `specification`: '$(specification_description)' + """ + + print(io, message) +end + """ An `Exception` to be thrown when a [`Diagram`](@ref) representing an invalid specification is passed to [`render`](@ref). diff --git a/test/assets/plantuml-example.puml b/test/assets/plantuml-example.puml new file mode 100644 index 0000000..bbc3901 --- /dev/null +++ b/test/assets/plantuml-example.puml @@ -0,0 +1,6 @@ +@startuml + +Bob -> Alice: Hi! +Alice -> Bob: Hi! + +@enduml diff --git a/test/runtests.jl b/test/runtests.jl index 291d09d..cc52f05 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,6 +6,7 @@ using Kroki: @mermaid_str, @plantuml_str, Diagram, + DiagramPathOrSpecificationError, InvalidDiagramSpecificationError, InvalidOutputFormatError, StatusError, # Imported from HTTP through Kroki @@ -43,6 +44,52 @@ function testShowMethodRenders( end @testset "Kroki" begin + @testset "`Diagram` instantiation providing" begin + @testset "`path` loads the file as the `specification" begin + diagram_path = joinpath(@__DIR__, "assets", "plantuml-example.puml") + expected_specification = read(diagram_path, String) + + diagram = Diagram(:plantml; path = diagram_path) + + @test diagram.specification === expected_specification + end + + @testset "`specification` stores it" begin + expected_specification = "A -> B: C" + + diagram = Diagram(:plantuml; specification = expected_specification) + + @test diagram.specification === expected_specification + end + + @testset "invalid `path`/`specification` combinations errors" begin + @testset "specifying both" begin + @test_throws( + DiagramPathOrSpecificationError, + Diagram(:mermaid; path = tempname(), specification = "A -> B: C") + ) + end + + @testset "specifying neither" begin + @test_throws(DiagramPathOrSpecificationError, Diagram(:svgbob)) + end + + @testset "rendering" begin + expected_specification = "X -> Y: Z" + + rendered_error = + sprint(showerror, DiagramPathOrSpecificationError(nothing, "X -> Y: Z")) + + @test startswith( + rendered_error, + "Either `path` or `specification` should be specified:", + ) + @test occursin("* `path`: ''", rendered_error) + @test occursin("* `specification`: '$(expected_specification)'", rendered_error) + end + end + end + @testset "`render`" begin # This is not an exhaustive list of supported diagram types or output # formats, but serves to verify generic rendering logic is available for at From 34aa0615d81d38c3f36947ec36a028d71d294d8b Mon Sep 17 00:00:00 2001 From: Joris Kraak Date: Tue, 12 Jul 2022 08:53:09 +0200 Subject: [PATCH 2/2] docs(examples): add section on loading specifications from files --- docs/src/examples.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/src/examples.md b/docs/src/examples.md index 1bb6010..ac6cdb9 100644 --- a/docs/src/examples.md +++ b/docs/src/examples.md @@ -147,6 +147,24 @@ svgbob""" """ ``` +### Loading from a file + +Instead of directly specifying a diagram, [`Diagram`](@ref)s can also load the +specifications from files. This is particularly useful when creating diagrams +using other tooling, e.g. [Structurizr](https://structurizr.com) or +[Excalidraw](https://excalidraw.com), or when sharing diagram definitions +across documentation. + +To load a diagram from a file, specify the path of the file as the `path` +keyword argument to [`Diagram`](@ref). + +```@example diagrams +Diagram( + :structurizr; + path = joinpath(@__DIR__, "..", "architecture", "workspace.dsl"), +) +``` + ## Rendering to a specific format To render to a specific format, explicitly call the [`render`](@ref) function