From e23ec345482fe623e4c99cafd8eacd8e548e9484 Mon Sep 17 00:00:00 2001 From: Michael Goerz Date: Sat, 27 Jan 2024 14:02:13 -0500 Subject: [PATCH] Revise TOML format Instead of an `[Inventory]` section, the TOML format now has to start with a comment line that declares the format version. The header fields `project` and `version` are now top-level keys. This looks cleaner, saves a few bytes, and introduces unique "magic bytes" at the beginning of the file so that an `inventory.toml` file could be recognized as a distinct fromat by FileIO.jl. Also, `version` is now optional. Most previous (experimental) `inventory.toml` files can still be read without error. --- .typos.toml | 9 ++ docs/src/formats.md | 17 ++-- docs/src/inventories/Documenter.toml | 3 +- docs/src/inventories/Julia.toml | 4 +- docs/src/inventories/JuliaDocs.toml | 4 +- src/toml_format.jl | 59 +++++------ test/test_inventory.jl | 143 +++++++++++++++++++++++++++ 7 files changed, 196 insertions(+), 43 deletions(-) create mode 100644 .typos.toml diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..c7b1da1 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,9 @@ +[default] +extend-ignore-identifiers-re = [ + "[A-Z]T", + "[0-9abcdef]{8}", + "[A-Z][a-z]+[A-Z]{2,5}[0-9]{4}", +] + +[files] +extend-exclude = ["*.bib", "*.toml", "test_inventory.jl"] diff --git a/docs/src/formats.md b/docs/src/formats.md index 85d3827..23d1151 100644 --- a/docs/src/formats.md +++ b/docs/src/formats.md @@ -35,23 +35,24 @@ Note that `DocInventories` internally uses the `text/x-intersphinx` MIME type fo ## TOML Format -The TOML format is another text output format that is optimized for human readability. It starts with a header section of the form +The TOML format is a text output format that is optimized for human readability. It starts with a header section of the form -``` -[Inventory] -format = "DocInventories v0" +```toml +# DocInventory version 0 project = "" version = "" ``` -The `format` line is mandatory and identifies the file as containing inventory data in the format described here. +The comment in the first line is mandatory and identifies the file as containing inventory data in the format described here. !!! warning - As indicated by the `v0` in the `format` line, the format described here is currently experimental and may change without notice + As indicated by the "version 0" in the header comment line, the format described here is currently experimental and may change without notice + +The `project` line must specify the name of the project described by the inventory. It is mandatory. The `version` line may specify the version of the project. It is optional, but recommended. After that, each [`InventoryItem`](@ref) is represented by a multi-line block of the form -``` +```toml [[.]] name = "" uri = "" @@ -61,7 +62,7 @@ priority = The four lines for `name`, `uri`, `dispname`, and `priority` may occur in any order. Also, for items with the default priority (`-1` for the `std` domain, `1` otherwise), the `priority` line may be omitted. If `dispname` is equal to `name` (usually indicated by `dispname="-"`), the `dispname` line may also be omitted. -The item-blocks may be grouped/separated by blank lines. In `.toml` file generated by `DocInventories.save("inventory.toml", inventory)` items will be grouped into blocks with the same `[[.]]` with a blank line between each block. +The item-blocks may be grouped/separated by blank lines. In `.toml` files generated by `DocInventories.save("inventory.toml", inventory)` items will be grouped into blocks with the same `[[.]]` with a blank line between each block. Any TOML parser should read a `.toml` file with the above structure into a nested dictionary, so that `item_dict = toml_data[domain][role][i]` corresponds to the `i`'th inventory item with the given `domain` and `role`. That `item_dict` will then map `"name"`, `"uri"`, and potentially `"dispname"` and `"priority"` to their respective values. diff --git a/docs/src/inventories/Documenter.toml b/docs/src/inventories/Documenter.toml index 08c16ff..90e02e8 100644 --- a/docs/src/inventories/Documenter.toml +++ b/docs/src/inventories/Documenter.toml @@ -1,5 +1,4 @@ -[Inventory] -format = "DocInventories v0" +# DocInventory version 0 project = "Documenter.jl" version = "1.2.1" diff --git a/docs/src/inventories/Julia.toml b/docs/src/inventories/Julia.toml index fd8b07d..bc98dc0 100644 --- a/docs/src/inventories/Julia.toml +++ b/docs/src/inventories/Julia.toml @@ -1,5 +1,5 @@ -[Inventory] -format = "DocInventories v0" +# DocInventory version 0 + project = "The Julia Language" version = "1.10.0" diff --git a/docs/src/inventories/JuliaDocs.toml b/docs/src/inventories/JuliaDocs.toml index 5492fc1..d4898fd 100644 --- a/docs/src/inventories/JuliaDocs.toml +++ b/docs/src/inventories/JuliaDocs.toml @@ -1,7 +1,5 @@ -[Inventory] -format = "DocInventories v0" +# DocInventory version 0 project = "JuliaDocs" -version = "" [[std.doc]] name = "DocumenterInterLinks" diff --git a/src/toml_format.jl b/src/toml_format.jl index c3f7a3a..f64ef14 100644 --- a/src/toml_format.jl +++ b/src/toml_format.jl @@ -25,47 +25,50 @@ function Base.show(io::IO, ::MIME"application/toml", inventory::Inventory) push!(domains[item.domain][item.role], item_data) end - toml_dict = Dict( - "Inventory" => Dict( - "format" => "DocInventories v0", - "project" => inventory.project, - "version" => inventory.version, - ), - domains... - ) + toml_dict = Dict("project" => inventory.project, domains...) + if !isempty(inventory.version) + toml_dict["version"] = inventory.version + end + TOML.println(io, "# DocInventory version 0") TOML.print(io, toml_dict; sorted=true) end function read_inventory(io::IO, ::MIME"application/toml") + format_header = readline(io) + if !startswith(format_header, "#") + @warn "Invalid format_header: $format_header" + # TODO: verify format_header more strictly once the format has settled. + end data = TOML.parse(io) try - inventory_format = data["Inventory"]["format"] - if inventory_format != "DocInventories v0" - msg = "Invalid inventory format: $(repr(inventory_format))" - throw(InventoryFormatError(msg)) - end - project = data["Inventory"]["project"] - version = data["Inventory"]["version"] + project = string(pop!(data, "project")) # mandatory + version = string(pop!(data, "version", "")) # optional items = InventoryItem[] for (domain, domain_data) in data - (domain == "Inventory") && continue # that's the header, not a domain - for (role, role_data) in domain_data - for item_data in role_data - push!( - items, - InventoryItem( - item_data["name"], - domain, - role, - get(item_data, "priority", (domain == "std") ? -1 : 1), - item_data["uri"], - get(item_data, "dispname", "-") + if domain_data isa Dict + for (role, role_data) in domain_data + for item_data in role_data + push!( + items, + InventoryItem( + item_data["name"], + domain, + role, + get(item_data, "priority", (domain == "std") ? -1 : 1), + item_data["uri"], + get(item_data, "dispname", "-") + ) ) - ) + end end + elseif domain == "format" + # For backward-compatibility, remove this `elseif` in v1.0 + @warn "Unexpected key: $domain" + else + throw(InventoryFormatError("Unexpected key: $domain")) end end return project, version, items diff --git a/test/test_inventory.jl b/test/test_inventory.jl index 6b79378..414d803 100644 --- a/test/test_inventory.jl +++ b/test/test_inventory.jl @@ -181,6 +181,118 @@ end @test c.value isa InventoryFormatError @test contains(c.output, "Unexpected line") + filename = joinpath(tempdir, "missing_project.toml") + #!format: off + write(filename, """ + # DocInventory version 0 + + [[std.doc]] + name = "DocumenterInterLinks" + uri = "DocumenterInterLinks.jl#readme" + """) + #!format: on + c = IOCapture.capture(rethrow=Union{}) do + Inventory(filename; root_url="") + end + @test c.value isa InventoryFormatError + @test contains(c.output, "key \"project\" not found") + + filename = joinpath(tempdir, "old_format.toml") + #!format: off + write(filename, """ + [Inventory] + format = "DocInventories v0" + project = "Test" + version = "0.1.0" + + [[std.doc]] + name = "DocumenterInterLinks" + uri = "DocumenterInterLinks.jl#readme" + """) + #!format: on + c = IOCapture.capture(rethrow=Union{}) do + Inventory(filename; root_url="") + end + @test_broken c.value isa InventoryFormatError + @test contains(c.output, "Unexpected key: format") + @test contains(c.output, "Invalid format_header: [Inventory]") + + filename = joinpath(tempdir, "old_format2.toml") + #!format: off + write(filename, """ + # This is an old inventory file, and because of this comment, the + # `[Inventory]` won't be stripped as a format_header + + [Inventory] + format = "DocInventories v0" + project = "Test" + version = "0.1.0" + + [[std.doc]] + name = "DocumenterInterLinks" + uri = "DocumenterInterLinks.jl#readme" + """) + #!format: on + c = IOCapture.capture(rethrow=Union{}) do + Inventory(filename; root_url="") + end + @test c.value isa InventoryFormatError + @test contains(c.output, "key \"project\" not found") + + filename = joinpath(tempdir, "no_header_line.toml") + #!format: off + write(filename, """ + project = "Test" + version = "0.1.0" + + [[std.doc]] + name = "DocumenterInterLinks" + uri = "DocumenterInterLinks.jl#readme" + """) + #!format: on + c = IOCapture.capture(rethrow=Union{}) do + Inventory(filename; root_url="") + end + @test c.value isa InventoryFormatError + @test contains(c.output, "Invalid format_header: project = \"Test\"") + @test contains(c.output, "key \"project\" not found") + + filename = joinpath(tempdir, "typo1.toml") + #!format: off + write(filename, """ + # DocInventory version 0 + project = "Test" + verison = "0.1.0" + + [[std.doc]] + name = "DocumenterInterLinks" + uri = "DocumenterInterLinks.jl#readme" + """) + #!format: on + c = IOCapture.capture(rethrow=Union{}) do + Inventory(filename; root_url="") + end + @test c.value isa InventoryFormatError + @test contains(c.output, "Unexpected key: verison") + + filename = joinpath(tempdir, "typo2.toml") + #!format: off + write(filename, """ + # DocInventory version 0 + project = "Test" + version = "0.1.0" + + [[std_doc]] + name = "DocumenterInterLinks" + uri = "DocumenterInterLinks.jl#readme" + """) + #!format: on + c = IOCapture.capture(rethrow=Union{}) do + Inventory(filename; root_url="") + end + @test c.value isa InventoryFormatError + @test contains(c.output, "Unexpected key: std_doc") + end end @@ -484,6 +596,37 @@ end @test contains(c.output, "Only v2 objects.inv files currently supported") @test c.value isa InventoryFormatError + filename = joinpath(tempdir, "wrong_version_type.toml") + #!format: off + write(filename, """ + # DocInventory version 0 + project = "Test" + version = 1.0 + + [[std.doc]] + name = "DocumenterInterLinks" + uri = "DocumenterInterLinks.jl#readme" + """) + #!format: on + readinv = Inventory(filename; root_url="") + # this still works because the float "1.0" is converted to string + @test readinv.version == "1.0" + + filename = joinpath(tempdir, "future_header.toml") + #!format: off + write(filename, """ + # Documenter Inventory version 1 + project = "Test" + version = "1.0" + + [[std.doc]] + name = "DocumenterInterLinks" + uri = "DocumenterInterLinks.jl#readme" + """) + #!format: on + readinv = Inventory(filename; root_url="") + @test readinv.project == "Test" + end end