Skip to content

Commit

Permalink
Merge pull request #12 from bauglir/support-loading-diagram-specifica…
Browse files Browse the repository at this point in the history
…tions-from-file

Support loading diagram specifications from file
  • Loading branch information
bauglir committed Jul 12, 2022
2 parents 29fcf2b + 34aa061 commit 7a6c022
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/src/api.md
Expand Up @@ -30,6 +30,7 @@ Filter = m -> endswith("$m", "_str")
## Private

```@docs
DiagramPathOrSpecificationError
InvalidDiagramSpecificationError
InvalidOutputFormatError
LIMITED_DIAGRAM_SUPPORT
Expand Down
18 changes: 18 additions & 0 deletions docs/src/examples.md
Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions src/Kroki.jl
Expand Up @@ -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
```
Expand All @@ -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 = "<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).
Expand Down
6 changes: 6 additions & 0 deletions test/assets/plantuml-example.puml
@@ -0,0 +1,6 @@
@startuml

Bob -> Alice: Hi!
Alice -> Bob: Hi!

@enduml
47 changes: 47 additions & 0 deletions test/runtests.jl
Expand Up @@ -6,6 +6,7 @@ using Kroki:
@mermaid_str,
@plantuml_str,
Diagram,
DiagramPathOrSpecificationError,
InvalidDiagramSpecificationError,
InvalidOutputFormatError,
StatusError, # Imported from HTTP through Kroki
Expand Down Expand Up @@ -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`: '<not specified>'", 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
Expand Down

0 comments on commit 7a6c022

Please sign in to comment.