From 1b87f8e3b09ce255ad997fba5e42da71ba238fa1 Mon Sep 17 00:00:00 2001 From: Milton Mazzarri Date: Thu, 18 May 2017 12:48:05 -0500 Subject: [PATCH 1/2] JSON formatter --- lib/ex_doc/cli.ex | 2 +- lib/ex_doc/formatter/json.ex | 152 +++++++++++++++++++++++++++ lib/ex_doc/retriever.ex | 61 +++++++++-- lib/mix/tasks/docs.ex | 2 +- mix.exs | 1 + mix.lock | 23 ++-- test/ex_doc/formatter/json_tests.exs | 62 +++++++++++ 7 files changed, 282 insertions(+), 21 deletions(-) create mode 100644 lib/ex_doc/formatter/json.ex create mode 100644 test/ex_doc/formatter/json_tests.exs diff --git a/lib/ex_doc/cli.ex b/lib/ex_doc/cli.ex index 2292eccd6..80d31ee8b 100644 --- a/lib/ex_doc/cli.ex +++ b/lib/ex_doc/cli.ex @@ -129,7 +129,7 @@ defmodule ExDoc.CLI do default: "PAGES" -i, --filter-prefix Include only modules that match the given prefix in the generated documentation. - -f, --formatter Docs formatter to use (html or epub), default: "html" + -f, --formatter Docs formatter to use (html, epub, or json), default: "html" -p, --homepage-url URL to link to for the site name --language Identify the primary language of the documents, its value must be a valid [BCP 47](https://tools.ietf.org/html/bcp47) language tag, default: "en" diff --git a/lib/ex_doc/formatter/json.ex b/lib/ex_doc/formatter/json.ex new file mode 100644 index 000000000..0dbaaae8c --- /dev/null +++ b/lib/ex_doc/formatter/json.ex @@ -0,0 +1,152 @@ +defmodule ExDoc.Formatter.JSON do + @moduledoc """ + Generate JSON documentation for Elixir projects + + The ExDoc JSON formatter starts with some information at the top: + + ## Top-level information + + * `about` - Indicates the version of the JSON structure format. + * `name` - Project name + * `version` - Project version + * `description` - Project summary description + * `homepage_url` - Specifies the project's home page + * `language` - Identifies the primary language of the documents. + * `icon` - Identifies the URL of the project's logo + * `items` - This JSON object contains modules, exceptions, protocols, + Mix tasks, extras, and attachments details. + + ## Modules, Exceptions, Protocols, Mix tasks + + Each component is an array that includes: + + * `module` - Module name + * `title` - Module title + * `doc` - Module documentation summary + * `doc_line` - Line number where the module documentation starts + * `source_path` - Path to the source code file in the project + * `source_url` - URL to the source code + * `types` - List of types + * `functions` - List of functions + * `callbacks` - List of callbacks + + ### Function, callback, and type details + + * `name` - Function name + * `arity`- Function arity + * `defaults` - Default argument values + * `doc` - Function documentation + * `doc_line` - Line number where the module documentation starts + * `source_path` - Path to the source code file in the project + * `source_url` - URL to the source code + * `signature` - Indicates the function signature + * `annotations` - Show annotations + + ## Extras + + This JSON object include the following fields: + + * `id` - Identifier + * `title` - Document title + * `group` - Specifies the group + * `content` - The document content in HTML format + + ## Attachments + + * `path` - Relative path + * `title` - Attachment title + * `size_in_bytes` - File size in bytes + + """ + + alias Mix.Project + alias ExDoc.Formatter.HTML + + @spec run(list, ExDoc.Config.t) :: String.t + def run(project_nodes, config) when is_map(config) do + config = + config + |> normalize_config() + |> output_setup() + + config + |> create_project_node(project_nodes) + |> Poison.encode!() + |> write!(config.output) + + Path.relative_to_cwd(config.output) + end + + defp normalize_config(config) do + config + |> Map.put(:output, Path.expand(config.output)) + |> Map.put(:name, (Project.config[:name] || config.project)) + |> Map.put(:description, Project.config[:description]) + end + + defp output_setup(config) do + file_name = config.name |> String.downcase() |> Kernel.<>(".json") + output = Path.join(config.output, file_name) + + if File.exists?(output) do + File.rm!(output) + else + File.mkdir_p!(config.output) + end + + %{config | output: output} + end + + defp create_project_node(config, project_nodes) do + %ExDoc.ProjectNode{ + name: config.name, + version: config.version, + homepage_url: config.homepage_url, + description: config.description, + icon: config.logo, + items: %{ + modules: filter_by_type(:module, project_nodes), + exceptions: filter_by_type(:exception, project_nodes), + protocols: filter_by_type(:protocol, project_nodes), + tasks: filter_by_type(:task, project_nodes), + extras: HTML.build_extras(project_nodes, config, ".html"), + attachments: extract_attachments_info(config), + }, + language: config.language, + } + end + + defp filter_by_type(type, project_nodes) do + type + |> HTML.filter_list(project_nodes) + |> Enum.map(fn mod -> + mod = + mod + |> Map.from_struct() + |> Map.merge(HTML.Templates.group_summary(mod)) + + struct(ExDoc.LeanModuleNode, mod) + end) + end + + defp write!(content, output) do + File.write!(output, content) + end + + defp extract_attachments_info(config) do + if path = config.assets do + path + |> Path.join("**/*") + |> Path.wildcard() + |> Enum.map(fn source -> + %{ + path: Path.join("assets", Path.relative_to(source, path)), + title: HTML.title_to_id(source), + size_in_bytes: File.stat!(source).size, + } + end) + else + [] + end + end +end diff --git a/lib/ex_doc/retriever.ex b/lib/ex_doc/retriever.ex index afb4b8fc3..3173b0014 100644 --- a/lib/ex_doc/retriever.ex +++ b/lib/ex_doc/retriever.ex @@ -1,10 +1,55 @@ +defmodule ExDoc.ProjectNode do + @moduledoc """ + Structure that represents a *project* + """ + + @derive [Poison.Encoder] + defstruct [:name, :version, :homepage_url, :description, :icon, :language, + :items, :attachments, :extras, about: "ExDoc/version/1"] + + @type t :: %__MODULE__{ + about: String.t, + name: String.t, + version: String.t, + homepage_url: String.t, + description: String.t, + icon: String.t, + language: String.t, + items: map + } +end + +defmodule ExDoc.LeanModuleNode do + @moduledoc """ + Alternative structure to represent a *module* + + See: `ExDoc.ModuleNode` is the original structure. + """ + + @derive [Poison.Encoder] + defstruct [:module, :doc, :doc_line, :source_path, :source_url, :title, + types: [], functions: [], callbacks: []] + + @type t :: %__MODULE__{ + title: nil | String.t, + module: nil | String.t, + types: list(), + functions: list(), + callbacks: list(), + doc: nil | String.t, + doc_line: non_neg_integer(), + source_path: nil | String.t, + source_url: nil | String.t + } +end + defmodule ExDoc.ModuleNode do @moduledoc """ Structure that represents a *module* """ - defstruct id: nil, title: nil, module: nil, doc: nil, doc_line: nil, - docs: [], typespecs: [], source_path: nil, source_url: nil, type: nil + defstruct [:id, :module, :doc, :doc_line, :source_path, :source_url, :type, + :title, docs: [], typespecs: []] @type t :: %__MODULE__{ id: nil | String.t, @@ -25,9 +70,9 @@ defmodule ExDoc.FunctionNode do Structure that holds all the elements of an individual *function* """ - defstruct id: nil, name: nil, arity: 0, defaults: [], doc: [], - type: nil, signature: nil, specs: [], annotations: [], - doc_line: nil, source_path: nil, source_url: nil + @derive {Poison.Encoder, except: [:id, :specs, :type]} + defstruct [:id, :name, :type, :signature, :doc_line, :source_path, + :source_url, arity: 0, defaults: [], doc: [], specs: [], annotations: []] @type t :: %__MODULE__{ id: nil | String.t, @@ -50,9 +95,9 @@ defmodule ExDoc.TypeNode do Structure that holds all the elements of an individual *type* """ - defstruct id: nil, name: nil, arity: 0, type: nil, doc_line: nil, - source_path: nil, source_url: nil, spec: nil, doc: nil, - signature: nil, annotations: [] + @derive {Poison.Encoder, except: [:id, :spec, :type]} + defstruct [:id, :name, :type, :signature, :doc_line, :source_path, + :source_url, :spec, :doc, annotations: [], arity: 0] @type t :: %__MODULE__{ id: nil | String.t, diff --git a/lib/mix/tasks/docs.ex b/lib/mix/tasks/docs.ex index f36dcab7c..6f3a0e162 100644 --- a/lib/mix/tasks/docs.ex +++ b/lib/mix/tasks/docs.ex @@ -70,7 +70,7 @@ defmodule Mix.Tasks.Docs do the generated documentation. Example: "MyApp.Core" * `:formatters` - Formatter to use; default: ["html"], - options: "html", "epub". + options: "html", "epub", "json". * `:language` - Identify the primary language of the documents, its value must be a valid [BCP 47](https://tools.ietf.org/html/bcp47) language tag; default: "en" diff --git a/mix.exs b/mix.exs index 365d99044..84348a6fb 100644 --- a/mix.exs +++ b/mix.exs @@ -22,6 +22,7 @@ defmodule ExDoc.Mixfile do defp deps do [{:earmark, "~> 1.1"}, + {:poison, "~> 3.0"}, {:markdown, github: "devinus/markdown", only: :test}, {:cmark, "~> 0.5", only: :test}, {:excoveralls, "~> 0.3", only: :test}] diff --git a/mix.lock b/mix.lock index 29ff4568b..f98b6e931 100644 --- a/mix.lock +++ b/mix.lock @@ -1,14 +1,15 @@ -%{"certifi": {:hex, :certifi, "1.0.0", "1c787a85b1855ba354f0b8920392c19aa1d06b0ee1362f9141279620a5be2039", [], [], "hexpm"}, - "cmark": {:hex, :cmark, "0.7.0", "cf20106714b35801df3a2250a8ca478366f93ad6067df9d6815c80b2606ae01e", [], [], "hexpm"}, - "earmark": {:hex, :earmark, "1.1.1", "433136b7f2e99cde88b745b3a0cfc3fbc81fe58b918a09b40fce7f00db4d8187", [:mix], [], "hexpm"}, - "excoveralls": {:hex, :excoveralls, "0.6.2", "0e993d096f1fbb6e70a3daced5c89aac066bda6bce57829622aa2d1e2b338cfb", [], [{:exjsx, "~> 3.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, - "exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, - "hackney": {:hex, :hackney, "1.7.1", "e238c52c5df3c3b16ce613d3a51c7220a784d734879b1e231c9babd433ac1cb4", [], [{:certifi, "1.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "4.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, +%{"certifi": {:hex, :certifi, "1.0.0", "1c787a85b1855ba354f0b8920392c19aa1d06b0ee1362f9141279620a5be2039", [:rebar3], []}, + "cmark": {:hex, :cmark, "0.7.0", "cf20106714b35801df3a2250a8ca478366f93ad6067df9d6815c80b2606ae01e", [:make, :mix], []}, + "earmark": {:hex, :earmark, "1.1.1", "433136b7f2e99cde88b745b3a0cfc3fbc81fe58b918a09b40fce7f00db4d8187", [:mix], []}, + "excoveralls": {:hex, :excoveralls, "0.6.2", "0e993d096f1fbb6e70a3daced5c89aac066bda6bce57829622aa2d1e2b338cfb", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]}, + "exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]}, + "hackney": {:hex, :hackney, "1.7.1", "e238c52c5df3c3b16ce613d3a51c7220a784d734879b1e231c9babd433ac1cb4", [:rebar3], [{:certifi, "1.0.0", [hex: :certifi, optional: false]}, {:idna, "4.0.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, "hoedown": {:git, "https://github.com/hoedown/hoedown.git", "980b9c549b4348d50b683ecee6abee470b98acda", []}, - "idna": {:hex, :idna, "4.0.0", "10aaa9f79d0b12cf0def53038547855b91144f1bfcc0ec73494f38bb7b9c4961", [], [], "hexpm"}, - "jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [], [], "hexpm"}, + "idna": {:hex, :idna, "4.0.0", "10aaa9f79d0b12cf0def53038547855b91144f1bfcc0ec73494f38bb7b9c4961", [:rebar3], []}, + "jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [:mix, :rebar3], []}, "markdown": {:git, "https://github.com/devinus/markdown.git", "d065dbcc4e242a85ca2516fdadd0082712871fd8", []}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [], [], "hexpm"}, - "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [], [], "hexpm"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [], [], "hexpm"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, + "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, + "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}, "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5", "2e73e068cd6393526f9fa6d399353d7c9477d6886ba005f323b592d389fb47be", [:make], []}} diff --git a/test/ex_doc/formatter/json_tests.exs b/test/ex_doc/formatter/json_tests.exs new file mode 100644 index 000000000..3239ac0dd --- /dev/null +++ b/test/ex_doc/formatter/json_tests.exs @@ -0,0 +1,62 @@ +defmodule ExDoc.Formatter.JSONTest do + use ExUnit.Case, async: true + + setup do + File.rm_rf(output_dir()) + File.mkdir_p!(output_dir()) + end + + def output_dir() do + Path.expand("../../tmp/json", __DIR__) + end + + def beam_dir() do + Path.expand("../../tmp/beam", __DIR__) + end + + defp doc_config() do + [project: "Elixir", + version: "1.0.1", + formatter: "json", + output: output_dir(), + source_root: beam_dir(), + source_beam: beam_dir(), + extras: ["test/fixtures/README.md"]] + end + + defp doc_config(config) do + Keyword.merge(doc_config(), config) + end + + defp generate_docs(config) do + ExDoc.generate_docs(config[:project], config[:version], config) + end + + defp decode(content) do + tree = %ExDoc.ProjectNode{ + items: %{ + modules: module_tree(), + exceptions: module_tree(), + protocols: module_tree(), + tasks: module_tree(), + } + } + Poison.decode!(content, as: tree) + end + + defp module_tree() do + [ + %ExDoc.LeanModuleNode{ + types: [%ExDoc.TypeNode{}], + functions: [%ExDoc.FunctionNode{}], + callbacks: [%ExDoc.FunctionNode{}], + } + ] + end + + test "run generates JSON content" do + generate_docs(doc_config([language: "fr"])) + + assert %ExDoc.ProjectNode{version: "1.0.1", name: "Elixir", language: "fr"} = output_dir() |> Path.join("elixir.json") |> File.read!() |> decode() + end +end From 79e049e55ad5d92c119609f804b22084ffe74dd3 Mon Sep 17 00:00:00 2001 From: Milton Mazzarri Date: Fri, 26 May 2017 23:01:15 -0500 Subject: [PATCH 2/2] Update mix.lock with latest version of Hex --- mix.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/mix.lock b/mix.lock index f98b6e931..f6b028302 100644 --- a/mix.lock +++ b/mix.lock @@ -1,15 +1,15 @@ -%{"certifi": {:hex, :certifi, "1.0.0", "1c787a85b1855ba354f0b8920392c19aa1d06b0ee1362f9141279620a5be2039", [:rebar3], []}, - "cmark": {:hex, :cmark, "0.7.0", "cf20106714b35801df3a2250a8ca478366f93ad6067df9d6815c80b2606ae01e", [:make, :mix], []}, - "earmark": {:hex, :earmark, "1.1.1", "433136b7f2e99cde88b745b3a0cfc3fbc81fe58b918a09b40fce7f00db4d8187", [:mix], []}, - "excoveralls": {:hex, :excoveralls, "0.6.2", "0e993d096f1fbb6e70a3daced5c89aac066bda6bce57829622aa2d1e2b338cfb", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]}, - "exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]}, - "hackney": {:hex, :hackney, "1.7.1", "e238c52c5df3c3b16ce613d3a51c7220a784d734879b1e231c9babd433ac1cb4", [:rebar3], [{:certifi, "1.0.0", [hex: :certifi, optional: false]}, {:idna, "4.0.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, +%{"certifi": {:hex, :certifi, "1.0.0", "1c787a85b1855ba354f0b8920392c19aa1d06b0ee1362f9141279620a5be2039", [:rebar3], [], "hexpm"}, + "cmark": {:hex, :cmark, "0.7.0", "cf20106714b35801df3a2250a8ca478366f93ad6067df9d6815c80b2606ae01e", [:make, :mix], [], "hexpm"}, + "earmark": {:hex, :earmark, "1.1.1", "433136b7f2e99cde88b745b3a0cfc3fbc81fe58b918a09b40fce7f00db4d8187", [:mix], [], "hexpm"}, + "excoveralls": {:hex, :excoveralls, "0.6.2", "0e993d096f1fbb6e70a3daced5c89aac066bda6bce57829622aa2d1e2b338cfb", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, + "hackney": {:hex, :hackney, "1.7.1", "e238c52c5df3c3b16ce613d3a51c7220a784d734879b1e231c9babd433ac1cb4", [:rebar3], [{:certifi, "1.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "4.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "hoedown": {:git, "https://github.com/hoedown/hoedown.git", "980b9c549b4348d50b683ecee6abee470b98acda", []}, - "idna": {:hex, :idna, "4.0.0", "10aaa9f79d0b12cf0def53038547855b91144f1bfcc0ec73494f38bb7b9c4961", [:rebar3], []}, - "jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [:mix, :rebar3], []}, + "idna": {:hex, :idna, "4.0.0", "10aaa9f79d0b12cf0def53038547855b91144f1bfcc0ec73494f38bb7b9c4961", [:rebar3], [], "hexpm"}, + "jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [:mix, :rebar3], [], "hexpm"}, "markdown": {:git, "https://github.com/devinus/markdown.git", "d065dbcc4e242a85ca2516fdadd0082712871fd8", []}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, - "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, - "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, + "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, + "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5", "2e73e068cd6393526f9fa6d399353d7c9477d6886ba005f323b592d389fb47be", [:make], []}}