Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Googly.DocumentAI.Model.GoogleCloudDocumentaiUiv1beta3Processor do

## Attributes

* `active_schema_version` (*type:* `String.t()`) - Optional. SchemaVersion used by the Processor. It is the same as Processor's DatasetSchema.schema_version Format is `projects/{project}/locations/{location}/schemas/{schema}/schemaVersions/{schema_version}
* `active_schema_version` (*type:* `String.t()`) - Optional. SchemaVersion used by the Processor. It is the same as Processor's DatasetSchema.schema_version Format is `projects/{project}/locations/{location}/schemas/{schema}/schemaVersions/{schema_version}`
* `create_time` (*type:* `DateTime.t()`) - Output only. The time the processor was created.
* `default_processor_version` (*type:* `String.t()`) - The default processor version.
* `display_name` (*type:* `String.t()`) - The display name of the processor.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Googly.DocumentAI.Model.GoogleCloudDocumentaiV1Processor do

## Attributes

* `active_schema_version` (*type:* `String.t()`) - Optional. SchemaVersion used by the Processor. It is the same as Processor's DatasetSchema.schema_version Format is `projects/{project}/locations/{location}/schemas/{schema}/schemaVersions/{schema_version}
* `active_schema_version` (*type:* `String.t()`) - Optional. SchemaVersion used by the Processor. It is the same as Processor's DatasetSchema.schema_version Format is `projects/{project}/locations/{location}/schemas/{schema}/schemaVersions/{schema_version}`
* `create_time` (*type:* `DateTime.t()`) - Output only. The time the processor was created.
* `default_processor_version` (*type:* `String.t()`) - The default processor version.
* `display_name` (*type:* `String.t()`) - The display name of the processor.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Googly.DocumentAI.Model.GoogleCloudDocumentaiV1ProcessorTypeLocationIn

## Attributes

* `location_id` (*type:* `String.t()`) - The location ID. For supported locations, refer to [regional and multi-regional support](/document-ai/docs/regions).
* `location_id` (*type:* `String.t()`) - The location ID. For supported locations, refer to [regional and multi-regional support](https://cloud.google.com/document-ai/docs/regions).
"""

defstruct [:location_id]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Googly.DocumentAI.Model.GoogleCloudDocumentaiV1beta3Processor do

## Attributes

* `active_schema_version` (*type:* `String.t()`) - Optional. SchemaVersion used by the Processor. It is the same as Processor's DatasetSchema.schema_version Format is `projects/{project}/locations/{location}/schemas/{schema}/schemaVersions/{schema_version}
* `active_schema_version` (*type:* `String.t()`) - Optional. SchemaVersion used by the Processor. It is the same as Processor's DatasetSchema.schema_version Format is `projects/{project}/locations/{location}/schemas/{schema}/schemaVersions/{schema_version}`
* `create_time` (*type:* `DateTime.t()`) - Output only. The time the processor was created.
* `default_processor_version` (*type:* `String.t()`) - The default processor version.
* `display_name` (*type:* `String.t()`) - The display name of the processor.
Expand Down
8 changes: 6 additions & 2 deletions lib/googly/generator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -107,20 +107,24 @@ defmodule Googly.Generator do
end

defp write_models(token) do
docs_link = Renderer.docs_link(token)

Enum.each(token.models, fn model ->
path = Path.join([token.lib_dir, "model", model.filename])
File.write!(path, Renderer.model(model, token.module_root))
File.write!(path, Renderer.model(model, token.module_root, docs_link))
end)

Logger.info("Wrote #{length(token.models)} models")
token
end

defp write_apis(token) do
docs_link = Renderer.docs_link(token)

Enum.each(token.apis, fn api ->
path = Path.join(token.lib_dir, api.filename)
File.mkdir_p!(Path.dirname(path))
File.write!(path, Renderer.api(api, token.module_root, token.global_params))
File.write!(path, Renderer.api(api, token.module_root, token.global_params, docs_link))
end)

Logger.info("Wrote #{length(token.apis)} resource modules")
Expand Down
56 changes: 50 additions & 6 deletions lib/googly/generator/renderer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,23 @@ defmodule Googly.Generator.Renderer do

@tpl Path.expand("../../../templates/client", __DIR__)

EEx.function_from_file(:def, :model, Path.join(@tpl, "model.ex.eex"), [:model, :root])
EEx.function_from_file(:def, :api, Path.join(@tpl, "api.ex.eex"), [:api, :root, :global_params])
# Fallback documentation host when a discovery doc omits `documentationLink`,
# and the base for absolutizing root-relative links in descriptions.
@default_docs_link "https://cloud.google.com/"

EEx.function_from_file(:def, :model, Path.join(@tpl, "model.ex.eex"), [
:model,
:root,
:docs_link
])

EEx.function_from_file(:def, :api, Path.join(@tpl, "api.ex.eex"), [
:api,
:root,
:global_params,
:docs_link
])

EEx.function_from_file(:def, :request, Path.join(@tpl, "request.ex.eex"), [:root, :base_url])
EEx.function_from_file(:def, :response, Path.join(@tpl, "response.ex.eex"), [:root])
EEx.function_from_file(:def, :decode, Path.join(@tpl, "decode.ex.eex"), [:root])
Expand Down Expand Up @@ -70,19 +85,47 @@ defmodule Googly.Generator.Renderer do

# -- template helpers -------------------------------------------------------

@doc "Formats a description for embedding in a heredoc `@doc`, indenting wrapped lines."
def doc(nil, _indent), do: ""
@doc """
Formats a description for embedding in a heredoc `@doc`, indenting wrapped
lines and repairing the Markdown quirks in Google's prose (see
`normalize_markdown_links/2` and `close_dangling_inline_code/1`). `base_url`
is the API's documentation host, used to absolutize root-relative links.
"""
def doc(str, indent, base_url \\ @default_docs_link)
def doc(nil, _indent, _base_url), do: ""

def doc(str, indent) do
def doc(str, indent, base_url) do
prefix = String.duplicate(" ", indent)

str
|> normalize_markdown_links(base_url)
|> close_dangling_inline_code()
|> String.replace("\\", "\\\\")
|> String.replace(~s("""), ~s(\\"\\"\\"))
|> String.replace("\#{", "\\\#{")
|> String.replace(~r/(\n+)([^\n])/, "\\1#{prefix}\\2")
end

# Google descriptions carry root-relative doc links (`](/foo/bar)`); ExDoc reads
# those as local file references and warns. Resolve each against the API's own
# documentation host (`base_url`) so it becomes a real URL — Cloud Storage lives
# under developers.google.com, most others under cloud.google.com. Absolute and
# protocol-relative (`//host`) targets are left alone.
defp normalize_markdown_links(str, base_url) do
Regex.replace(~r{\]\((/(?!/)[^)\s]*)\)}, str, fn _match, path ->
"](" <> URI.to_string(URI.merge(base_url, path)) <> ")"
end)
end

# Some descriptions open an inline `code` span they never close, which makes
# EarmarkParser (ExDoc) warn about "unclosed backquotes". This is upstream-prose
# repair, not real Markdown parsing (not worth a parser dependency): when the
# backtick count is odd, append one to close the dangling span.
defp close_dangling_inline_code(str) do
odd? = str |> String.graphemes() |> Enum.count(&(&1 == "`")) |> rem(2) == 1
if odd?, do: str <> "`", else: str
end

@doc "True when a model needs the `Decode` helper aliased."
def uses_decode?(model) do
Enum.any?(model.properties, fn p ->
Expand Down Expand Up @@ -192,7 +235,8 @@ defmodule Googly.Generator.Renderer do
if String.contains?(raw, "Google"), do: raw, else: "Google " <> raw
end

defp docs_link(token), do: token.rest[:documentationLink] || "https://cloud.google.com/"
@doc "The API's documentation URL, or the Google-docs fallback."
def docs_link(token), do: token.rest[:documentationLink] || @default_docs_link

defp description(token) do
base = "#{title(token)} client library."
Expand Down
8 changes: 4 additions & 4 deletions templates/client/api.ex.eex
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

defmodule <%= root %>.<%= api.name %> do
@moduledoc """
<%= doc(api.description, 2) %>
<%= doc(api.description, 2, docs_link) %>
"""

alias <%= root %>.Request
<%= for ep <- api.endpoints do %>
@doc """
<%= doc(ep.description, 2) %>
<%= doc(ep.description, 2, docs_link) %>

## Parameters
<%= for p <- ep.required_parameters do %>
* `<%= p.variable_name %>` (*type:* `<%= p.type.typespec %>`) - <%= doc(p.description, 6) %><% end %>
* `<%= p.variable_name %>` (*type:* `<%= p.type.typespec %>`) - <%= doc(p.description, 6, docs_link) %><% end %>
* `opts` (*type:* `keyword()`) - Query and call options (`:token`, plus any of the below)<%= for p <- optional_params(ep, global_params) do %>
* `<%= p.name %>` (*type:* `<%= p.type.typespec %>`) - <%= doc(p.description, 10) %><% end %>
* `<%= p.name %>` (*type:* `<%= p.type.typespec %>`) - <%= doc(p.description, 10, docs_link) %><% end %>

## Returns

Expand Down
4 changes: 2 additions & 2 deletions templates/client/model.ex.eex
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

defmodule <%= root %>.Model.<%= Macro.camelize(model.name) %> do
@moduledoc """
<%= doc(model.description, 2) %><%= unless model.properties == [] do %>
<%= doc(model.description, 2, docs_link) %><%= unless model.properties == [] do %>

## Attributes
<%= for p <- model.properties do %>
* `<%= p.name %>` (*type:* `<%= p.type.typespec %>`) - <%= doc(p.description, 6) %><% end %><% end %>
* `<%= p.name %>` (*type:* `<%= p.type.typespec %>`) - <%= doc(p.description, 6, docs_link) %><% end %><% end %>
"""
<%= if uses_decode?(model) do %>
alias <%= root %>.Decode
Expand Down
30 changes: 30 additions & 0 deletions test/googly/generator/renderer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,36 @@ defmodule Googly.Generator.RendererTest do
test "indents wrapped lines" do
assert Renderer.doc("line1\nline2", 2) == "line1\n line2"
end

test "closes an unterminated inline code span (ExDoc: unclosed backquotes)" do
# Google descriptions sometimes open a `code` span they never close.
assert Renderer.doc("Format is `projects/{project}/schemaVersions/{v}", 0) ==
"Format is `projects/{project}/schemaVersions/{v}`"
end

test "leaves balanced backticks untouched" do
assert Renderer.doc("call `foo`, such as: `OCR`, `INVOICE`.", 0) ==
"call `foo`, such as: `OCR`, `INVOICE`."
end

test "absolutizes root-relative links against the default docs host" do
assert Renderer.doc("see [regions](/document-ai/docs/regions).", 0) ==
"see [regions](https://cloud.google.com/document-ai/docs/regions)."
end

test "absolutizes root-relative links against the API's own docs host" do
# Cloud Storage's documentationLink lives under developers.google.com, not
# cloud.google.com — so the base URL must come from the discovery doc.
base = "https://developers.google.com/storage/docs/json_api/"

assert Renderer.doc("see [buckets](/storage/docs/buckets)", 0, base) ==
"see [buckets](https://developers.google.com/storage/docs/buckets)"
end

test "leaves absolute and protocol-relative links untouched" do
assert Renderer.doc("[x](https://example.com/y) and [z](//cdn/z)", 0) ==
"[x](https://example.com/y) and [z](//cdn/z)"
end
end

describe "wire_map/1" do
Expand Down
Loading