spikard_cli/init/
elixir.rs1use super::scaffolder::{ProjectScaffolder, ScaffoldedFile};
4use anyhow::Result;
5use heck::{ToPascalCase, ToSnakeCase};
6use std::path::{Path, PathBuf};
7
8pub 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}