Skip to main content

spikard_cli/init/
elixir.rs

1//! Elixir project scaffolder for Spikard applications.
2
3use super::scaffolder::{ProjectScaffolder, ScaffoldedFile};
4use anyhow::Result;
5use heck::{ToPascalCase, ToSnakeCase};
6use std::path::{Path, PathBuf};
7
8/// Elixir project scaffolder.
9pub struct ElixirScaffolder;
10
11impl ElixirScaffolder {
12    fn app_name(project_name: &str) -> String {
13        project_name.replace('-', "_").to_snake_case()
14    }
15
16    fn module_name(project_name: &str) -> String {
17        Self::app_name(project_name).to_pascal_case()
18    }
19
20    fn generate_mix_exs(&self, project_name: &str) -> String {
21        let app_name = Self::app_name(project_name);
22        let module_name = Self::module_name(project_name);
23        let version = env!("CARGO_PKG_VERSION");
24
25        format!(
26            r#"defmodule {module_name}.MixProject do
27  use Mix.Project
28
29  def project do
30    [
31      app: :{app_name},
32      version: "0.1.0",
33      elixir: "~> 1.18",
34      start_permanent: Mix.env() == :prod,
35      deps: deps()
36    ]
37  end
38
39  def application do
40    [
41      extra_applications: [:logger]
42    ]
43  end
44
45  defp deps do
46    [
47      {{:spikard, "~> {version}"}},
48      {{:jason, "~> 1.4"}}
49    ]
50  end
51end
52"#
53        )
54    }
55
56    fn generate_formatter_exs(&self) -> String {
57        r#"[
58  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
59  line_length: 120
60]
61"#
62        .to_string()
63    }
64
65    fn generate_app_module(&self, project_name: &str) -> String {
66        let module_name = Self::module_name(project_name);
67
68        format!(
69            r#"defmodule {module_name} do
70  @moduledoc "Generated Spikard application scaffold."
71
72  alias {module_name}.Router
73
74  @spec start(keyword()) :: {{:ok, Spikard.server_handle()}} | {{:error, String.t()}}
75  def start(opts \\ []) do
76    defaults = [port: 4000, host: "127.0.0.1"]
77    Spikard.start(Router, Keyword.merge(defaults, opts))
78  end
79end
80"#
81        )
82    }
83
84    fn generate_router(&self, project_name: &str) -> String {
85        let module_name = Self::module_name(project_name);
86
87        format!(
88            r#"defmodule {module_name}.Router do
89  @moduledoc "Generated Spikard router scaffold."
90
91  use Spikard.Router
92
93  get("/health", &health/1)
94
95  @spec health(Spikard.Request.t()) :: Spikard.Response.t()
96  def health(_request) do
97    Spikard.Response.json(%{{status: "ok"}})
98  end
99end
100"#
101        )
102    }
103
104    fn generate_test(&self, project_name: &str) -> String {
105        let module_name = Self::module_name(project_name);
106
107        format!(
108            r#"defmodule {module_name}.RouterTest do
109  use ExUnit.Case, async: true
110
111  alias {module_name}
112  alias {module_name}.Router
113
114  test "application module exposes a start function" do
115    assert function_exported?({module_name}, :start, 1)
116  end
117
118  test "router exposes the generated health route" do
119    routes = Router.routes()
120
121    assert Enum.any?(routes, fn route ->
122             route.method == "GET" and route.path == "/health"
123           end)
124  end
125end
126"#
127        )
128    }
129
130    fn generate_run_script(&self, project_name: &str) -> String {
131        let module_name = Self::module_name(project_name);
132
133        format!(
134            r#"{{:ok, _server}} = {module_name}.start(port: 4000, host: "127.0.0.1")
135Process.sleep(:infinity)
136"#
137        )
138    }
139
140    fn generate_gitignore(&self) -> String {
141        r#"/_build/
142/deps/
143/cover/
144/.elixir_ls/
145/.lexical/
146erl_crash.dump
147*.ez
148.DS_Store
149"#
150        .to_string()
151    }
152}
153
154impl ProjectScaffolder for ElixirScaffolder {
155    fn scaffold(&self, _project_dir: &Path, project_name: &str) -> Result<Vec<ScaffoldedFile>> {
156        let app_name = Self::app_name(project_name);
157
158        Ok(vec![
159            ScaffoldedFile::new(PathBuf::from("mix.exs"), self.generate_mix_exs(project_name)),
160            ScaffoldedFile::new(PathBuf::from(".formatter.exs"), self.generate_formatter_exs()),
161            ScaffoldedFile::new(
162                PathBuf::from(format!("lib/{app_name}.ex")),
163                self.generate_app_module(project_name),
164            ),
165            ScaffoldedFile::new(
166                PathBuf::from(format!("lib/{app_name}/router.ex")),
167                self.generate_router(project_name),
168            ),
169            ScaffoldedFile::new(
170                PathBuf::from(format!("test/{}_test.exs", app_name)),
171                self.generate_test(project_name),
172            ),
173            ScaffoldedFile::new(PathBuf::from("test/test_helper.exs"), "ExUnit.start()\n".to_string()),
174            ScaffoldedFile::new(PathBuf::from("run.exs"), self.generate_run_script(project_name)),
175            ScaffoldedFile::new(PathBuf::from(".gitignore"), self.generate_gitignore()),
176        ])
177    }
178
179    fn next_steps(&self, project_name: &str) -> Vec<String> {
180        vec![
181            format!("cd {}", project_name),
182            "mix deps.get".to_string(),
183            "mix test".to_string(),
184            "mix run --no-halt run.exs".to_string(),
185        ]
186    }
187}