Skip to content

Commit c06889e

Browse files
committed
Add systemd unit name helpers
1 parent ccd2d38 commit c06889e

4 files changed

Lines changed: 154 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- Added `Systemd.UnitName` helpers for formatting typed, template, and instance unit names.
6+
37
## v0.1.1
48

59
- Renamed the OTP application from `:systemd` to `:systemdkit` so dependencies can use the normal `{:systemdkit, "~> 0.1.1"}` form.

lib/systemd/unit_name.ex

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
defmodule Systemd.UnitName do
2+
@moduledoc """
3+
Pure helpers for formatting systemd unit names.
4+
5+
Systemd unit names are ordinary strings, but callers often need to format the
6+
same patterns repeatedly: typed names such as `dbus.service`, template names
7+
such as `my_app@.service`, and instance names such as `my_app@4000.service`.
8+
9+
This module keeps that formatting explicit without introducing a struct or a
10+
deployment-specific abstraction.
11+
12+
## Examples
13+
14+
iex> Systemd.UnitName.new("dbus", :service)
15+
"dbus.service"
16+
17+
iex> Systemd.UnitName.template("my_app", :service)
18+
"my_app@.service"
19+
20+
iex> Systemd.UnitName.instance("my_app", 4000, :service)
21+
"my_app@4000.service"
22+
23+
iex> Systemd.UnitName.ensure_type("my_app@4000", :service)
24+
"my_app@4000.service"
25+
26+
iex> Systemd.UnitName.drop_type("my_app@4000.service")
27+
"my_app@4000"
28+
"""
29+
30+
@type unit_type :: :service | :socket | :timer | :target | :mount | :path | String.t()
31+
32+
@doc """
33+
Formats a typed systemd unit name.
34+
35+
If `name` already ends with the requested type suffix, it is returned
36+
unchanged.
37+
"""
38+
@spec new(String.t(), unit_type()) :: String.t()
39+
def new(name, type) when is_binary(name), do: ensure_type(name, type)
40+
41+
@doc """
42+
Formats a systemd template unit name.
43+
"""
44+
@spec template(String.t(), unit_type()) :: String.t()
45+
def template(name, type) when is_binary(name) do
46+
name
47+
|> base_name()
48+
|> Kernel.<>("@")
49+
|> ensure_type(type)
50+
end
51+
52+
@doc """
53+
Formats a systemd instance unit name.
54+
"""
55+
@spec instance(String.t(), String.Chars.t(), unit_type()) :: String.t()
56+
def instance(name, instance, type) when is_binary(name) do
57+
base = base_name(name)
58+
instance = to_string(instance)
59+
60+
base
61+
|> Kernel.<>("@" <> instance)
62+
|> ensure_type(type)
63+
end
64+
65+
@doc """
66+
Ensures a unit name has the suffix for `type`.
67+
"""
68+
@spec ensure_type(String.t(), unit_type()) :: String.t()
69+
def ensure_type(name, type) when is_binary(name) do
70+
suffix = suffix(type)
71+
72+
if String.ends_with?(name, suffix) do
73+
name
74+
else
75+
name <> suffix
76+
end
77+
end
78+
79+
@doc """
80+
Drops the final systemd unit type suffix from a name.
81+
"""
82+
@spec drop_type(String.t()) :: String.t()
83+
def drop_type(name) when is_binary(name) do
84+
Enum.reduce_while(known_suffixes(), name, fn suffix, name ->
85+
if String.ends_with?(name, suffix) do
86+
{:halt, String.trim_trailing(name, suffix)}
87+
else
88+
{:cont, name}
89+
end
90+
end)
91+
end
92+
93+
defp base_name(name) do
94+
name
95+
|> drop_type()
96+
|> String.trim_trailing("@")
97+
end
98+
99+
defp known_suffixes do
100+
~w(.service .socket .timer .target .mount .path)
101+
end
102+
103+
defp suffix(type) when is_atom(type), do: "." <> Atom.to_string(type)
104+
105+
defp suffix(type) when is_binary(type) do
106+
type = String.trim_leading(type, ".")
107+
"." <> type
108+
end
109+
end

mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ defmodule Systemd.MixProject do
7676
Systemd.Properties
7777
],
7878
"Unit files": [
79+
Systemd.UnitName,
7980
Systemd.UnitFile,
8081
Systemd.UnitFile.Blank,
8182
Systemd.UnitFile.Builder,

test/systemd/unit_name_test.exs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
defmodule Systemd.UnitNameTest do
2+
use ExUnit.Case, async: true
3+
4+
doctest Systemd.UnitName
5+
6+
alias Systemd.UnitName
7+
8+
test "formats typed unit names" do
9+
assert UnitName.new("dbus", :service) == "dbus.service"
10+
assert UnitName.new("dbus.service", :service) == "dbus.service"
11+
assert UnitName.new("timers", :target) == "timers.target"
12+
end
13+
14+
test "formats template unit names" do
15+
assert UnitName.template("my_app", :service) == "my_app@.service"
16+
assert UnitName.template("my_app.service", :service) == "my_app@.service"
17+
assert UnitName.template("my_app@.service", :service) == "my_app@.service"
18+
assert UnitName.template("socket_app", :socket) == "socket_app@.socket"
19+
end
20+
21+
test "formats instance unit names" do
22+
assert UnitName.instance("my_app", 4000, :service) == "my_app@4000.service"
23+
assert UnitName.instance("my_app.service", "blue", :service) == "my_app@blue.service"
24+
assert UnitName.instance("my_app@.service", "blue", :service) == "my_app@blue.service"
25+
assert UnitName.instance("timer_app", "daily", :timer) == "timer_app@daily.timer"
26+
end
27+
28+
test "ensures unit type suffix" do
29+
assert UnitName.ensure_type("my_app@4000", :service) == "my_app@4000.service"
30+
assert UnitName.ensure_type("my_app@4000.service", :service) == "my_app@4000.service"
31+
assert UnitName.ensure_type("custom", "service") == "custom.service"
32+
assert UnitName.ensure_type("custom", ".service") == "custom.service"
33+
end
34+
35+
test "drops known unit type suffixes" do
36+
assert UnitName.drop_type("my_app@4000.service") == "my_app@4000"
37+
assert UnitName.drop_type("timers.target") == "timers"
38+
assert UnitName.drop_type("plain") == "plain"
39+
end
40+
end

0 commit comments

Comments
 (0)