Skip to content

Commit

Permalink
feat(sdk/elixir): rework on Elixir codegen
Browse files Browse the repository at this point in the history
Signed-off-by: Thanabodee Charoenpiriyakij <wingyminus@gmail.com>
  • Loading branch information
wingyplus committed Feb 22, 2024
1 parent 31ddf27 commit a895daa
Show file tree
Hide file tree
Showing 146 changed files with 5,986 additions and 5,992 deletions.
31 changes: 18 additions & 13 deletions internal/mage/sdk/elixir.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ const (
)

// https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=debian-buster
var elixirVersions = []string{"1.14.5", "1.15.4"}
var elixirVersions = []string{"1.16.0", "1.15.7", "1.14.5"}

const (
otpVersion = "25.3.2.4"
debianVersion = "20230612"
otpVersion = "26.2.1"
debianVersion = "20231009"
)

var _ SDK = Elixir{}
Expand Down Expand Up @@ -124,19 +124,28 @@ func (Elixir) Generate(ctx context.Context) error {

cliBinPath := "/.dagger-cli"

generated := elixirBase(c, elixirVersions[1]).
generated, err := elixirBase(c, elixirVersions[0]).
WithServiceBinding("dagger-engine", devEngine).
WithEnvVariable("_EXPERIMENTAL_DAGGER_RUNNER_HOST", endpoint).
WithMountedFile(cliBinPath, util.DevelDaggerBinary(ctx, c)).
WithEnvVariable("_EXPERIMENTAL_DAGGER_CLI_BIN", cliBinPath).
WithExec([]string{"mix", "dagger.gen"})
WithExec([]string{"mix", "run", "scripts/fetch_introspection.exs"}).
WithWorkdir("dagger_codegen").
WithExec([]string{"mix", "deps.get"}).
WithExec([]string{"mix", "escript.build"}).
WithExec([]string{"./dagger_codegen", "generate", "--introspection", "../introspection.json", "--outdir", "gen"}).
WithExec([]string{"mix", "format", "gen/*.ex"}).
Sync(ctx)
if err != nil {
return err
}

if err := os.RemoveAll(elixirSDKGeneratedPath); err != nil {
return err
}

ok, err := generated.
Directory(strings.Replace(elixirSDKGeneratedPath, elixirSDKPath+"/", "", 1)).
Directory("gen").
Export(ctx, elixirSDKGeneratedPath)
if err != nil {
return err
Expand Down Expand Up @@ -206,16 +215,12 @@ func (Elixir) Bump(ctx context.Context, engineVersion string) error {
}

func elixirBase(c *dagger.Client, elixirVersion string) *dagger.Container {
const appDir = "sdk/elixir"

src := c.Directory().WithDirectory("/", util.Repository(c).Directory(appDir))

mountPath := fmt.Sprintf("/%s", appDir)
mountPath := fmt.Sprintf("/%s", elixirSDKPath)

return c.Container().
From(fmt.Sprintf("hexpm/elixir:%s-erlang-%s-debian-buster-%s-slim", elixirVersion, otpVersion, debianVersion)).
From(fmt.Sprintf("hexpm/elixir:%s-erlang-%s-debian-bookworm-%s-slim", elixirVersion, otpVersion, debianVersion)).
WithWorkdir(mountPath).
WithDirectory(mountPath, src).
WithDirectory(mountPath, c.Directory().WithDirectory("/", util.Repository(c).Directory(elixirSDKPath))).
WithExec([]string{"mix", "local.hex", "--force"}).
WithExec([]string{"mix", "local.rebar", "--force"}).
WithExec([]string{"mix", "deps.get"})
Expand Down
4 changes: 4 additions & 0 deletions sdk/elixir/dagger_codegen/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
29 changes: 29 additions & 0 deletions sdk/elixir/dagger_codegen/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
dagger_codegen-*.tar

# Temporary files, for example, from tests.
/tmp/

# Escript
dagger_codegen
12 changes: 12 additions & 0 deletions sdk/elixir/dagger_codegen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Dagger.Codegen

This packages provides a tool to generate Elixir code Dagger GraphQL.

## How to build

Run the following commands below:

```
$ mix deps.get
$ mix escript.build
```
9 changes: 9 additions & 0 deletions sdk/elixir/dagger_codegen/lib/dagger/codegen.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule Dagger.Codegen do
@moduledoc """
Functions for generating code from Dagger GraphQL.
"""

def generate(generator, introspection_schema) do
generator.generate(introspection_schema)
end
end
51 changes: 51 additions & 0 deletions sdk/elixir/dagger_codegen/lib/dagger/codegen/cli.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
defmodule Dagger.Codegen.CLI do
@moduledoc """
Main entrypoint for Dagger codegen binary.
"""

def main(args) do
:argparse.run(Enum.map(args, &String.to_charlist/1), cli(), %{progname: :dagger_codegen})
end

defp cli() do
%{
commands: %{
~c"generate" => %{
arguments: [
%{
name: :outdir,
type: :binary,
long: ~c"-outdir",
required: true
},
%{
name: :introspection,
type: :binary,
long: ~c"-introspection",
required: true
}
],
handler: &handle_generate/1
}
}
}
end

def handle_generate(%{outdir: outdir, introspection: introspection}) do
%{"__schema" => schema} = introspection |> File.read!() |> Jason.decode!()

IO.puts("Generate code to #{Path.expand(outdir)}")

File.mkdir_p!(outdir)

Dagger.Codegen.generate(
Dagger.Codegen.ElixirGenerator,
Nestru.decode_from_map!(schema, Dagger.Codegen.Introspection.Types.Schema)
)
|> Enum.flat_map(& &1)
|> Enum.each(fn {file, code} ->
Path.join(outdir, file)
|> File.write!(code)
end)
end
end
49 changes: 49 additions & 0 deletions sdk/elixir/dagger_codegen/lib/dagger/codegen/elixir_generator.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
defmodule Dagger.Codegen.ElixirGenerator do
@moduledoc """
Dagger Elixir code generator.
"""

alias Dagger.Codegen.ElixirGenerator.Formatter
alias Dagger.Codegen.Introspection.Types.InputValue
alias Dagger.Codegen.Introspection.Types.TypeRef
alias Dagger.Codegen.Introspection.Visitor
alias Dagger.Codegen.Introspection.VisitorHandlers

require EEx

@template_dir Path.join([:code.priv_dir(:dagger_codegen), "templates", "elixir"])

@scalar_template Path.join(@template_dir, "scalar.eex")
@object_template Path.join(@template_dir, "object.eex")
@input_template Path.join(@template_dir, "input.eex")
@enum_template Path.join(@template_dir, "enum.eex")

EEx.function_from_file(:defp, :scalar_template, @scalar_template, [:assigns])
EEx.function_from_file(:defp, :object_template, @object_template, [:assigns])
EEx.function_from_file(:defp, :input_template, @input_template, [:assigns])
EEx.function_from_file(:defp, :enum_template, @enum_template, [:assigns])

def generate(schema) do
generate_code(schema)
end

defp generate_code(schema) do
handlers = %VisitorHandlers{
scalar: fn type ->
{"#{Formatter.format_var_name(type.name)}.ex", scalar_template(%{type: type})}
end,
object: fn type ->
{"#{Formatter.format_var_name(type.name)}.ex",
object_template(%{type: type, schema: schema})}
end,
input: fn type ->
{"#{Formatter.format_var_name(type.name)}.ex", input_template(%{type: type})}
end,
enum: fn type ->
{"#{Formatter.format_var_name(type.name)}.ex", enum_template(%{type: type})}
end
}

Visitor.visit(schema, handlers)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
defmodule Dagger.Codegen.ElixirGenerator.Formatter do
alias Dagger.Codegen.Introspection.Types.TypeRef

def format_module("Query"), do: format_module("Client")

def format_module(name) do
Module.concat(Dagger, Macro.camelize(name))
|> to_string()
|> String.trim_leading("Elixir.")
end

def format_var_name("Query"), do: format_var_name("Client")

def format_var_name(name) do
Macro.underscore(name)
end

# Temporarily fixes for issue https://github.com/dagger/dagger/issues/6310.
@acronym_words %{
"GPU" => "Gpu",
"VCS" => "Vcs"
}

def format_function_name(name, acronym_words \\ @acronym_words) do
acronym_words
|> Enum.reduce(name, fn {word, new_word}, name ->
String.replace(name, word, new_word)
end)
|> Macro.underscore()
end

def format_doc(doc) do
doc = String.replace(doc, "\"", "\\\"")

for [text, api] <- Regex.scan(~r/`(?<name>[a-zA-Z0-9]+)`/, doc),
reduce: doc do
reason -> String.replace(reason, text, "`#{format_function_name(api)}`")
end
end

def format_type(%TypeRef{
kind: "LIST",
of_type: %TypeRef{kind: "NON_NULL", of_type: type}
}) do
"[#{format_type(type, false)}]"
end

def format_type(%TypeRef{kind: "NON_NULL", of_type: type}) do
if type.kind == "LIST" do
format_type(type)
else
format_type(type, false)
end
end

def format_type(%TypeRef{} = type) do
format_type(type, true)
end

defp format_type(%TypeRef{kind: "SCALAR", name: name}, nullable?) do
type =
case name do
"String" -> "String.t()"
"Int" -> "integer()"
"Float" -> "float()"
"Boolean" -> "boolean()"
"DateTime" -> "DateTime.t()"
otherwise -> "#{format_module(otherwise)}.t()"
end

if nullable? do
"#{type} | nil"
else
type
end
end

# OBJECT, INPUT_OBJECT, ENUM
defp format_type(%TypeRef{name: name}, nullable?) do
type = "#{format_module(name)}.t()"

if nullable? do
"#{type} | nil"
else
type
end
end

def format_typespec_output_type(
%TypeRef{
kind: "NON_NULL",
of_type: %TypeRef{kind: "SCALAR"}
} = type
) do
"{:ok, #{format_type(type)}} | {:error, term()}"
end

def format_typespec_output_type(
%TypeRef{
kind: "SCALAR"
} = type
) do
"{:ok, #{format_type(type)}} | {:error, term()}"
end

def format_typespec_output_type(%TypeRef{
kind: "NON_NULL",
of_type: %TypeRef{kind: "LIST"} = type
}) do
"{:ok, #{format_type(type)}} | {:error, term()}"
end

def format_typespec_output_type(
%TypeRef{
kind: "LIST"
} = type
) do
"{:ok, #{format_type(type)}} | {:error, term()}"
end

def format_typespec_output_type(type) do
format_type(type)
end

# TODO: clary which pattern match use.
def format_output_type(%TypeRef{
kind: "NON_NULL",
of_type: %TypeRef{kind: "LIST", of_type: type}
}) do
format_output_type(type)
end

def format_output_type(%TypeRef{
kind: "NON_NULL",
of_type: type
}) do
format_output_type(type)
end

def format_output_type(%TypeRef{
kind: "LIST",
of_type: type
}) do
format_output_type(type)
end

def format_output_type(%TypeRef{kind: "OBJECT", name: name}) do
format_module(name)
end

def format_output_type(_type_ref), do: "DaggerInvalidOutput"
end

0 comments on commit a895daa

Please sign in to comment.