Skip to content

Commit

Permalink
Reimplement generator in Elixir (googleapis#1327)
Browse files Browse the repository at this point in the history
* Use discovery api

* Fix Tesla usage

* Fix tesla error handling

* Put the tesla dependency back

* Add stub Elixir generator

* Add basic generator test for reading all schemas

* output files to directory

* Fill out some descriptions

* Use token pattern

* Get the right model properties

* lint

* Add elixir template

* Stub for generating API modules

* Build apis

* Refactor to split generator models

* Refactor Type

* Refactor types

* Fix date type

* Mix format

* Fix license headers

* Refactor Token

* Add tests for parsing parameters

* Fix param types

* Fix path parameters

* Handle global optional params

* Formatting

* Cleanup and docs

* Type test

* Add model test

* Refactor model to pass tests

* Cleanup

* Add test for loading nested unnamed schemas

* Fix test for nested model names

* We're ok fixing the naming scheme for nested models

* Fix style

* Generate all apis, fix typespec for endpoint with no params

* Fix variable name in apis

* Fix collection of nested methods

* Handle no return type for methods

* Typespec for no return type endpoint should be 'nil'

* Fix list types for parameters

* Add request body param

* Add test for no parameters

* Handle overwriting the request parameter name

* Pass ResourceContext when parsing parameters

* ResourceContext can now track the base_path and handles path generation

* Endpoint.from_discovery_method now returns a list of Endpoints

* Handle supportsMediaUpload

* Write the connection.ex file. Fix baseUrl for media upload clients

* Fix upload type and return type

* Fix indentation

* Fix double slash in resource paths

* Ensure leading directories exist

* Fix map types

* Handle google-datetime types

* Handle nested array/map types

* Fix nested model names

* Fix handling Date class

* Fix only URI encode string params in paths

* Fix uri escaping

* Handle dataWrapped apis

* Handle resources with no methods

* Make all license headers 2019

* Fix copyright header to use Google LLC
  • Loading branch information
chingor13 authored Jun 14, 2019
1 parent 3f30eae commit 9d355fa
Show file tree
Hide file tree
Showing 24 changed files with 2,492 additions and 9 deletions.
2 changes: 1 addition & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ use Mix.Config

config :google_apis,
spec_converter: GoogleApis.Converter.ApiSpecConverter,
client_generator: GoogleApis.Generator.SwaggerCli,
hex_api_key: System.get_env("HEX_API_KEY") || "invalidkey",
client_generator: GoogleApis.Generator.ElixirGenerator,
swagger_cli_image: "swaggerapi/swagger-codegen-cli:v2.3.1",
oauth_client: System.get_env("GOOGLE_CLIENT_ID"),
oauth_secret: System.get_env("GOOGLE_CLIENT_SECRET"),
Expand Down
189 changes: 189 additions & 0 deletions lib/google_apis/generator/elixir_generator.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# Copyright 2019 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

defmodule GoogleApis.Generator.ElixirGenerator do
@moduledoc """
Code generator written in Elixir which takes a Google Discovery document and
generates an Elixir client library.
"""

@behaviour GoogleApis.Generator
alias GoogleApis.ApiConfig
alias GoogleApi.Discovery.V1.Model.{JsonSchema, RestDescription}

alias GoogleApis.Generator.ElixirGenerator.{
Api,
Endpoint,
Model,
Parameter,
Renderer,
ResourceContext,
Token
}

@doc """
Run the generator for the specified api configuration
"""
@spec generate_client(ApiConfig.t()) :: {:ok, any()} | {:error, String.t()}
def generate_client(api_config) do
Token.build(api_config)
|> load_models
|> update_model_properties
|> create_directories
|> write_model_files
|> load_global_optional_params
|> load_apis
|> write_api_files
|> write_connection
end

defp load_models(token) do
models = all_models(token.rest_description)

token
|> Map.put(:models, models)
|> Map.put(
:models_by_name,
Enum.reduce(models, %{}, fn model, acc -> Map.put(acc, model.name, model) end)
)
end

defp update_model_properties(token) do
Map.update!(token, :models, fn models ->
models
|> Enum.map(fn model ->
Model.update_properties(model, token.resource_context)
end)
end)
end

defp create_directories(token) do
IO.puts("Creating leading directories")
File.mkdir_p!(Path.join(token.base_dir, "api"))
File.mkdir_p!(Path.join(token.base_dir, "model"))
token
end

defp write_connection(token) do
scopes = token.rest_description.auth.oauth2.scopes
otp_app = "google_api_#{Macro.underscore(token.rest_description.name)}"

path = Path.join(token.base_dir, "connection.ex")
IO.puts("Writing connection.ex.")

File.write!(
path,
Renderer.connection(token.namespace, scopes, otp_app, token.base_url)
)
end

defp write_model_files(%{models: models, namespace: namespace, base_dir: base_dir} = token) do
models
|> Enum.each(fn model ->
path = Path.join([base_dir, "model", Model.filename(model)])
IO.puts("Writing #{model.name} to #{path}.")

File.write!(
path,
Renderer.model(model, namespace)
)
end)

token
end

defp load_global_optional_params(token) do
params = token.rest_description.parameters || []

global_optional_parameters =
params
|> Enum.map(fn {name, schema} ->
Parameter.from_json_schema(name, schema, token.resource_context)
end)
|> Enum.sort_by(fn param -> param.name end)

Map.put(token, :global_optional_parameters, global_optional_parameters)
end

defp load_apis(token) do
Map.put(token, :apis, all_apis(token.rest_description, token.resource_context))
end

defp write_api_files(token) do
token.apis
|> Enum.each(fn api ->
path = Path.join([token.base_dir, "api", Api.filename(api)])
IO.puts("Writing #{api.name} to #{path}.")

File.write!(
path,
Renderer.api(api, token.namespace, token.global_optional_parameters, token.data_wrapped)
)
end)

token
end

@doc """
Returns all Apis found from the provided RestDescription
"""
@spec all_apis(RestDescription.t()) :: list(Api.t())
def all_apis(rest_description) do
all_apis(rest_description, ResourceContext.default())
end

@doc """
Returns all Apis found from the provided RestDescription and ResourceContext
"""
@spec all_apis(RestDescription.t(), ResourceContext.t()) :: list(Api.t())
def all_apis(%{resources: resources}, context) do
resources
|> Enum.map(fn {name, resource} ->
name = Macro.camelize(name)
methods = collect_methods(resource)

%Api{
name: name,
description: "API calls for all endpoints tagged `#{name}`.",
endpoints:
Enum.flat_map(methods, fn {_, method} ->
Endpoint.from_discovery_method(method, context)
end)
}
end)
end

defp collect_methods(%{resources: resources, methods: methods}) do
collect_methods_from_methods(methods) ++ collect_methods_from_resources(resources)
end

defp collect_methods_from_methods(nil), do: []
defp collect_methods_from_methods(methods), do: Enum.into(methods, [])

defp collect_methods_from_resources(nil), do: []

defp collect_methods_from_resources(resources) do
Enum.flat_map(resources, fn {name, resource} ->
collect_methods(resource)
end)
end

@doc """
Returns all Models found from the provided RestDescription
"""
@spec all_models(RestDescription.t()) :: list(Model.t())
def all_models(rest_description) do
Model.from_schemas(rest_description.schemas)
end
end
35 changes: 35 additions & 0 deletions lib/google_apis/generator/elixir_generator/api.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright 2019 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

defmodule GoogleApis.Generator.ElixirGenerator.Api do
@moduledoc """
An Api represents a collection of endpoints.
"""

@type t :: %__MODULE__{
:name => String.t(),
:description => String.t(),
:endpoints => list(Endpoint.t())
}

defstruct [:name, :description, :endpoints]

@doc """
Returns the name of the file that should be generated.
"""
@spec filename(t) :: String.t()
def filename(api) do
"#{Macro.underscore(api.name)}.ex"
end
end
Loading

0 comments on commit 9d355fa

Please sign in to comment.