wheneverd 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/README.md +45 -2
  4. data/lib/wheneverd/dsl/period_parser.rb +8 -107
  5. data/lib/wheneverd/dsl/period_strategy/array_strategy.rb +29 -0
  6. data/lib/wheneverd/dsl/period_strategy/base.rb +65 -0
  7. data/lib/wheneverd/dsl/period_strategy/duration_strategy.rb +33 -0
  8. data/lib/wheneverd/dsl/period_strategy/string_strategy.rb +51 -0
  9. data/lib/wheneverd/dsl/period_strategy/symbol_strategy.rb +31 -0
  10. data/lib/wheneverd/dsl/period_strategy.rb +43 -0
  11. data/lib/wheneverd/duration.rb +1 -7
  12. data/lib/wheneverd/errors.rb +3 -0
  13. data/lib/wheneverd/interval.rb +22 -7
  14. data/lib/wheneverd/systemd/cron_parser/dow_parser.rb +208 -0
  15. data/lib/wheneverd/systemd/cron_parser/field_parser.rb +163 -0
  16. data/lib/wheneverd/systemd/cron_parser.rb +56 -303
  17. data/lib/wheneverd/systemd/renderer.rb +6 -64
  18. data/lib/wheneverd/systemd/unit_content_builder.rb +76 -0
  19. data/lib/wheneverd/systemd/unit_deleter.rb +2 -28
  20. data/lib/wheneverd/systemd/unit_lister.rb +2 -28
  21. data/lib/wheneverd/systemd/unit_namer.rb +6 -14
  22. data/lib/wheneverd/systemd/unit_path_utils.rb +54 -0
  23. data/lib/wheneverd/systemd/unit_writer.rb +2 -28
  24. data/lib/wheneverd/trigger/base.rb +22 -0
  25. data/lib/wheneverd/trigger/boot.rb +8 -6
  26. data/lib/wheneverd/trigger/calendar.rb +7 -0
  27. data/lib/wheneverd/trigger/interval.rb +8 -6
  28. data/lib/wheneverd/validation.rb +89 -0
  29. data/lib/wheneverd/version.rb +1 -1
  30. data/lib/wheneverd.rb +4 -1
  31. data/test/domain_model_test.rb +105 -0
  32. data/test/systemd_cron_parser_test.rb +41 -25
  33. data/test/systemd_renderer_errors_test.rb +1 -1
  34. metadata +13 -1
@@ -1,42 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "cron_parser/field_parser"
4
+ require_relative "cron_parser/dow_parser"
5
+
3
6
  module Wheneverd
4
7
  module Systemd
5
8
  # Converts 5-field cron expressions into systemd `OnCalendar=` specs.
9
+ #
10
+ # Uses {FieldParser} for parsing numeric fields (minute, hour, day-of-month, month)
11
+ # and {DowParser} for day-of-week parsing and formatting.
6
12
  module CronParser
13
+ MONTH_NAMES = {
14
+ "jan" => 1,
15
+ "feb" => 2,
16
+ "mar" => 3,
17
+ "apr" => 4,
18
+ "may" => 5,
19
+ "jun" => 6,
20
+ "jul" => 7,
21
+ "aug" => 8,
22
+ "sep" => 9,
23
+ "oct" => 10,
24
+ "nov" => 11,
25
+ "dec" => 12
26
+ }.freeze
27
+
7
28
  # @param cron_5_fields [String]
8
29
  # @return [Array<String>] systemd `OnCalendar=` values
9
30
  # @raise [Wheneverd::Systemd::UnsupportedCronError]
10
31
  def self.to_on_calendar_values(cron_5_fields)
11
32
  input = cron_5_fields.to_s.strip
12
- minute_str, hour_str, dom_str, month_str, dow_str = split_fields(input)
13
-
14
- minute = parse_numeric_expression(minute_str, 0..59, field: "minute", input: input, pad: 2)
15
- hour = parse_numeric_expression(hour_str, 0..23, field: "hour", input: input, pad: 2)
16
- dom = parse_numeric_expression(dom_str, 1..31, field: "day-of-month", input: input, pad: 0)
17
- month = parse_month_expression(month_str, input: input)
18
- dow_set = parse_dow_set(dow_str, input: input)
19
-
20
- time = "#{hour}:#{minute}:00"
21
- dom_any = dom == "*"
22
- month_any = month == "*"
23
-
24
- date = "*-#{month_any ? '*' : month}-#{dom_any ? '*' : dom}"
25
-
26
- if dow_set.nil? && dom_any
27
- return ["*-#{month_any ? '*' : month}-* #{time}"]
28
- end
29
-
30
- values = []
31
-
32
- if dow_set
33
- dow = format_dow_set(dow_set)
34
- values << "#{dow} *-#{month_any ? '*' : month}-* #{time}"
35
- end
33
+ fields = split_fields(input)
36
34
 
37
- values << "#{date} #{time}" unless dom_any
38
-
39
- values
35
+ parsed = parse_all_fields(fields, input)
36
+ format_on_calendar_values(parsed)
40
37
  end
41
38
 
42
39
  # @param cron_5_fields [String]
@@ -52,301 +49,57 @@ module Wheneverd
52
49
  raise UnsupportedCronError, message
53
50
  end
54
51
 
52
+ ParsedFields = Struct.new(:minute, :hour, :dom, :month, :dow_set, keyword_init: true)
53
+
55
54
  def self.split_fields(input)
56
55
  parts = input.split(/\s+/)
57
- validate_parts_length(parts, input: input)
56
+ unless parts.length == 5
57
+ raise UnsupportedCronError, "Unsupported cron #{input.inspect}: expected 5 fields"
58
+ end
58
59
 
59
60
  parts
60
61
  end
61
62
  private_class_method :split_fields
62
63
 
63
- def self.validate_parts_length(parts, input:)
64
- return if parts.length == 5
65
-
66
- raise UnsupportedCronError, "Unsupported cron #{input.inspect}: expected 5 fields"
67
- end
68
- private_class_method :validate_parts_length
69
-
70
- MONTH_NAMES = {
71
- "jan" => 1,
72
- "feb" => 2,
73
- "mar" => 3,
74
- "apr" => 4,
75
- "may" => 5,
76
- "jun" => 6,
77
- "jul" => 7,
78
- "aug" => 8,
79
- "sep" => 9,
80
- "oct" => 10,
81
- "nov" => 11,
82
- "dec" => 12
83
- }.freeze
84
-
85
- DOW_NAMES = {
86
- "sun" => 0,
87
- "mon" => 1,
88
- "tue" => 2,
89
- "wed" => 3,
90
- "thu" => 4,
91
- "fri" => 5,
92
- "sat" => 6
93
- }.freeze
94
-
95
- DOW_SYSTEMD = {
96
- 0 => "Sun",
97
- 1 => "Mon",
98
- 2 => "Tue",
99
- 3 => "Wed",
100
- 4 => "Thu",
101
- 5 => "Fri",
102
- 6 => "Sat"
103
- }.freeze
104
-
105
- DOW_ORDER = [1, 2, 3, 4, 5, 6, 0].freeze
106
-
107
- def self.parse_month_expression(month_str, input:)
108
- parse_mapped_numeric_expression(
109
- month_str,
110
- 1..12,
111
- field: "month",
112
- input: input,
113
- names: MONTH_NAMES
64
+ def self.parse_all_fields(fields, input)
65
+ minute_str, hour_str, dom_str, month_str, dow_str = fields
66
+
67
+ ParsedFields.new(
68
+ minute: FieldParser.parse_numeric(minute_str, 0..59, field: "minute", input: input,
69
+ pad: 2),
70
+ hour: FieldParser.parse_numeric(hour_str, 0..23, field: "hour", input: input, pad: 2),
71
+ dom: FieldParser.parse_numeric(dom_str, 1..31, field: "day-of-month", input: input,
72
+ pad: 0),
73
+ month: FieldParser.parse_mapped(month_str, 1..12, field: "month", input: input,
74
+ names: MONTH_NAMES),
75
+ dow_set: DowParser.parse(dow_str, input: input)
114
76
  )
115
77
  end
116
- private_class_method :parse_month_expression
117
-
118
- def self.parse_numeric_expression(str, range, field:, input:, pad:)
119
- expr = parse_mapped_numeric_expression(str, range, field: field, input: input, names: {})
120
- return expr if pad <= 0 || expr == "*"
121
-
122
- pad_expression_numbers(expr, pad: pad)
123
- end
124
- private_class_method :parse_numeric_expression
125
-
126
- def self.parse_mapped_numeric_expression(str, range, field:, input:, names:)
127
- raw = str.to_s.strip
128
- if raw.empty?
129
- raise UnsupportedCronError,
130
- "Unsupported cron #{input.inspect}: #{field} is empty"
131
- end
132
-
133
- parts = raw.split(",").map(&:strip)
134
- return "*" if parts.any? { |part| part == "*" }
135
-
136
- parts.map do |part|
137
- mapped_part_to_systemd(part, range, field: field, input: input, names: names)
138
- end.join(",")
139
- end
140
- private_class_method :parse_mapped_numeric_expression
141
-
142
- def self.mapped_part_to_systemd(part, range, field:, input:, names:)
143
- base, step_str = part.split("/", 2)
144
- step = if step_str.nil?
145
- nil
146
- else
147
- parse_positive_int(step_str, field: field, input: input,
148
- label: "step")
149
- end
150
-
151
- base_expr =
152
- if base == "*"
153
- range.begin.to_s
154
- elsif (match = /\A(?<start>[^-]+)-(?<finish>[^-]+)\z/.match(base))
155
- start_value = parse_mapped_value(match[:start], range, field: field, input: input,
156
- names: names)
157
- finish_value = parse_mapped_value(match[:finish], range, field: field, input: input,
158
- names: names)
159
- unless start_value <= finish_value
160
- raise UnsupportedCronError,
161
- "Unsupported cron #{input.inspect}: invalid #{field} range"
162
- end
163
-
164
- "#{start_value}..#{finish_value}"
165
- else
166
- parse_mapped_value(base, range, field: field, input: input, names: names).to_s
167
- end
168
-
169
- return base_expr if step.nil?
170
-
171
- "#{base_expr}/#{step}"
172
- end
173
- private_class_method :mapped_part_to_systemd
174
-
175
- def self.parse_mapped_value(token, range, field:, input:, names:)
176
- raw = token.to_s.strip
177
- if raw.empty?
178
- raise UnsupportedCronError,
179
- "Unsupported cron #{input.inspect}: empty #{field} token"
180
- end
181
-
182
- if /\A\d+\z/.match?(raw)
183
- value = Integer(raw, 10)
184
- unless range.cover?(value)
185
- raise UnsupportedCronError, "Unsupported cron #{input.inspect}: #{field} out of range"
186
- end
187
-
188
- return value
189
- end
190
-
191
- key = raw.downcase
192
- if names.key?(key)
193
- value = names.fetch(key)
194
- return value if range.cover?(value)
195
- end
196
-
197
- raise UnsupportedCronError, "Unsupported cron #{input.inspect}: invalid #{field} token"
198
- end
199
- private_class_method :parse_mapped_value
200
-
201
- def self.pad_expression_numbers(expr, pad:)
202
- expr.gsub(%r{(?<![/\d])\d+}) { |m| m.rjust(pad, "0") }
203
- end
204
- private_class_method :pad_expression_numbers
205
-
206
- def self.parse_positive_int(str, field:, input:, label:)
207
- unless /\A\d+\z/.match?(str)
208
- raise UnsupportedCronError,
209
- "Unsupported cron #{input.inspect}: #{field} #{label} must be a number"
210
- end
211
-
212
- value = Integer(str, 10)
213
- unless value.positive?
214
- raise UnsupportedCronError,
215
- "Unsupported cron #{input.inspect}: #{field} #{label} must be positive"
216
- end
217
-
218
- value
219
- end
220
- private_class_method :parse_positive_int
221
-
222
- def self.parse_dow_set(dow_str, input:)
223
- raw = dow_str.to_s.strip
224
- if raw.empty?
225
- raise UnsupportedCronError,
226
- "Unsupported cron #{input.inspect}: day-of-week is empty"
227
- end
228
- return nil if raw == "*"
229
-
230
- present = Array.new(7, false)
231
-
232
- raw.split(",").map(&:strip).each do |part|
233
- apply_dow_part(present, part, input: input)
234
- end
235
-
236
- days = present.each_index.select { |idx| present[idx] }
237
- return nil if days.length == 7
238
-
239
- days
240
- end
241
- private_class_method :parse_dow_set
242
-
243
- def self.apply_dow_part(present, part, input:)
244
- if part.empty?
245
- raise UnsupportedCronError,
246
- "Unsupported cron #{input.inspect}: invalid day-of-week token"
247
- end
78
+ private_class_method :parse_all_fields
248
79
 
249
- base, step_str = part.split("/", 2)
250
- step = if step_str.nil?
251
- nil
252
- else
253
- parse_positive_int(step_str, field: "day-of-week",
254
- input: input, label: "step")
255
- end
80
+ def self.format_on_calendar_values(parsed)
81
+ time = "#{parsed.hour}:#{parsed.minute}:00"
82
+ dom_any = parsed.dom == "*"
83
+ month_any = parsed.month == "*"
256
84
 
257
- sequence =
258
- if base == "*"
259
- (0..6).to_a
260
- elsif (match = /\A(?<start>[^-]+)-(?<finish>[^-]+)\z/.match(base))
261
- dow_range_sequence(match[:start], match[:finish], input: input)
262
- else
263
- start_day = parse_dow_value(base, input: input)
264
- start_day = 0 if start_day == 7
265
- step ? (start_day..6).to_a : [start_day]
266
- end
85
+ date = "*-#{month_any ? '*' : parsed.month}-#{dom_any ? '*' : parsed.dom}"
267
86
 
268
- if step
269
- sequence.each_with_index { |day, idx| present[day] = true if (idx % step).zero? }
270
- else
271
- sequence.each { |day| present[day] = true }
87
+ if parsed.dow_set.nil? && dom_any
88
+ return ["*-#{month_any ? '*' : parsed.month}-* #{time}"]
272
89
  end
273
- end
274
- private_class_method :apply_dow_part
275
-
276
- def self.dow_range_sequence(start_token, finish_token, input:)
277
- start_raw = parse_dow_value(start_token, input: input)
278
- finish_raw = parse_dow_value(finish_token, input: input)
279
-
280
- start_day = start_raw == 7 ? 0 : start_raw
281
- finish_day = finish_raw == 7 ? 0 : finish_raw
282
-
283
- return (0..6).to_a if start_raw.zero? && finish_raw == 7
284
-
285
- return (start_day..6).to_a + [0] if finish_raw == 7 && !start_raw.zero?
286
-
287
- return (start_day..finish_day).to_a if start_day <= finish_day
288
-
289
- (start_day..6).to_a + (0..finish_day).to_a
290
- end
291
- private_class_method :dow_range_sequence
292
90
 
293
- def self.parse_dow_value(token, input:)
294
- raw = token.to_s.strip
295
- if raw.empty?
296
- raise UnsupportedCronError,
297
- "Unsupported cron #{input.inspect}: invalid day-of-week token"
298
- end
299
-
300
- if /\A\d+\z/.match?(raw)
301
- value = Integer(raw, 10)
302
- return value if value.between?(0, 7)
303
-
304
- raise UnsupportedCronError, "Unsupported cron #{input.inspect}: day-of-week out of range"
305
- end
91
+ values = []
306
92
 
307
- unless /\A[A-Za-z]+\z/.match?(raw)
308
- raise UnsupportedCronError, "Unsupported cron #{input.inspect}: invalid day-of-week token"
93
+ if parsed.dow_set
94
+ dow = DowParser.format(parsed.dow_set)
95
+ values << "#{dow} *-#{month_any ? '*' : parsed.month}-* #{time}"
309
96
  end
310
97
 
311
- key = raw.downcase
312
- key = key[0, 3] if key.length > 3
313
- return DOW_NAMES.fetch(key) if DOW_NAMES.key?(key)
314
-
315
- raise UnsupportedCronError, "Unsupported cron #{input.inspect}: invalid day-of-week token"
316
- end
317
- private_class_method :parse_dow_value
318
-
319
- def self.format_dow_set(days)
320
- present = Array.new(7, false)
321
- days.each { |day| present[day] = true }
322
-
323
- tokens = []
324
- i = 0
325
- while i < DOW_ORDER.length
326
- day = DOW_ORDER.fetch(i)
327
- unless present.fetch(day)
328
- i += 1
329
- next
330
- end
331
-
332
- start = day
333
- j = i
334
- j += 1 while (j + 1) < DOW_ORDER.length && present.fetch(DOW_ORDER.fetch(j + 1))
335
- finish = DOW_ORDER.fetch(j)
336
-
337
- token =
338
- if start == finish
339
- DOW_SYSTEMD.fetch(start)
340
- else
341
- "#{DOW_SYSTEMD.fetch(start)}..#{DOW_SYSTEMD.fetch(finish)}"
342
- end
343
- tokens << token
344
- i = j + 1
345
- end
98
+ values << "#{date} #{time}" unless dom_any
346
99
 
347
- tokens.join(",")
100
+ values
348
101
  end
349
- private_class_method :format_dow_set
102
+ private_class_method :format_on_calendar_values
350
103
  end
351
104
  end
352
105
  end
@@ -25,10 +25,10 @@ module Wheneverd
25
25
  # `-N` suffix is appended to avoid collisions.
26
26
  #
27
27
  # The identifier is sanitized for use in filenames (invalid characters become `-`).
28
+ #
29
+ # Uses {UnitContentBuilder} for generating unit file contents.
28
30
  class Renderer
29
31
  MARKER_PREFIX = "# Generated by wheneverd (wheneverd)"
30
- SERVICE_SECTION = ["[Service]", "Type=oneshot"].freeze
31
- TIMER_SUFFIX = ["Persistent=true", "", "[Install]", "WantedBy=timers.target", ""].freeze
32
32
 
33
33
  # Render a schedule to `systemd` units.
34
34
  #
@@ -37,7 +37,7 @@ module Wheneverd
37
37
  # @return [Array<Unit>]
38
38
  def self.render(schedule, identifier:)
39
39
  validate_schedule(schedule)
40
- id = sanitize_identifier(identifier)
40
+ id = UnitPathUtils.sanitize_identifier(identifier)
41
41
  render_schedule(schedule, id)
42
42
  end
43
43
 
@@ -74,78 +74,20 @@ module Wheneverd
74
74
  Unit.new(
75
75
  path_basename: path_basename,
76
76
  kind: :service,
77
- contents: service_contents(path_basename, job.command)
77
+ contents: UnitContentBuilder.service_contents(path_basename, job.command)
78
78
  )
79
79
  end
80
80
  private_class_method :build_service_unit
81
81
 
82
82
  def self.build_timer_unit(path_basename, trigger)
83
+ timer_lines = UnitContentBuilder.timer_lines_for(trigger)
83
84
  Unit.new(
84
85
  path_basename: path_basename,
85
86
  kind: :timer,
86
- contents: timer_contents(path_basename, timer_lines_for(trigger))
87
+ contents: UnitContentBuilder.timer_contents(path_basename, timer_lines)
87
88
  )
88
89
  end
89
90
  private_class_method :build_timer_unit
90
-
91
- def self.timer_lines_for(trigger)
92
- case trigger
93
- when Wheneverd::Trigger::Interval
94
- ["OnActiveSec=#{trigger.seconds}", "OnUnitActiveSec=#{trigger.seconds}"]
95
- when Wheneverd::Trigger::Boot
96
- ["OnBootSec=#{trigger.seconds}"]
97
- when Wheneverd::Trigger::Calendar then calendar_timer_lines(trigger)
98
- else raise ArgumentError, "Unsupported trigger type: #{trigger.class}"
99
- end
100
- end
101
- private_class_method :timer_lines_for
102
-
103
- def self.calendar_timer_lines(trigger)
104
- trigger.on_calendar.flat_map do |spec|
105
- CalendarSpec.to_on_calendar_values(spec).map { |value| "OnCalendar=#{value}" }
106
- end
107
- end
108
- private_class_method :calendar_timer_lines
109
-
110
- def self.service_contents(path_basename, command)
111
- ([
112
- marker,
113
- "[Unit]",
114
- "Description=wheneverd job #{path_basename}",
115
- ""
116
- ] + SERVICE_SECTION + ["ExecStart=#{command}", ""]).join("\n")
117
- end
118
- private_class_method :service_contents
119
-
120
- def self.timer_contents(path_basename, timer_lines)
121
- ([
122
- marker,
123
- "[Unit]",
124
- "Description=wheneverd timer #{path_basename}",
125
- "",
126
- "[Timer]"
127
- ] + timer_lines + TIMER_SUFFIX).join("\n")
128
- end
129
- private_class_method :timer_contents
130
-
131
- def self.marker
132
- "#{MARKER_PREFIX} #{Wheneverd::VERSION}; do not edit."
133
- end
134
- private_class_method :marker
135
-
136
- def self.sanitize_identifier(identifier)
137
- raw = identifier.to_s.strip
138
- raise InvalidIdentifierError, "identifier must not be empty" if raw.empty?
139
-
140
- sanitized = raw.gsub(/[^A-Za-z0-9_-]/, "-").gsub(/-+/, "-").gsub(/\A-|-+\z/, "")
141
- if sanitized.empty?
142
- raise InvalidIdentifierError,
143
- "identifier must include at least one alphanumeric character"
144
- end
145
-
146
- sanitized
147
- end
148
- private_class_method :sanitize_identifier
149
91
  end
150
92
  end
151
93
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wheneverd
4
+ module Systemd
5
+ # Builds the text content for systemd unit files.
6
+ #
7
+ # Handles the structure of service and timer unit files, including
8
+ # the marker header, sections, and proper formatting.
9
+ module UnitContentBuilder
10
+ SERVICE_SECTION = ["[Service]", "Type=oneshot"].freeze
11
+ TIMER_SUFFIX = ["Persistent=true", "", "[Install]", "WantedBy=timers.target", ""].freeze
12
+
13
+ # Build service unit file contents.
14
+ #
15
+ # @param path_basename [String] unit file name for description
16
+ # @param command [String] ExecStart command
17
+ # @return [String] complete unit file contents
18
+ def self.service_contents(path_basename, command)
19
+ build_unit(
20
+ description: "wheneverd job #{path_basename}",
21
+ sections: SERVICE_SECTION + ["ExecStart=#{command}", ""]
22
+ )
23
+ end
24
+
25
+ # Build timer unit file contents.
26
+ #
27
+ # @param path_basename [String] unit file name for description
28
+ # @param timer_lines [Array<String>] timer configuration lines (OnCalendar, OnActiveSec, etc.)
29
+ # @return [String] complete unit file contents
30
+ def self.timer_contents(path_basename, timer_lines)
31
+ build_unit(
32
+ description: "wheneverd timer #{path_basename}",
33
+ sections: ["[Timer]"] + timer_lines + TIMER_SUFFIX
34
+ )
35
+ end
36
+
37
+ # Build timer lines for a trigger.
38
+ #
39
+ # @param trigger [Wheneverd::Trigger::Base] the trigger
40
+ # @return [Array<String>] timer configuration lines
41
+ # @raise [ArgumentError] if trigger type is unsupported
42
+ def self.timer_lines_for(trigger)
43
+ # Calendar triggers need special handling to convert DSL specs to systemd specs
44
+ return calendar_timer_lines(trigger) if trigger.is_a?(Wheneverd::Trigger::Calendar)
45
+
46
+ unless trigger.respond_to?(:systemd_timer_lines)
47
+ raise ArgumentError, "Unsupported trigger type: #{trigger.class}"
48
+ end
49
+
50
+ trigger.systemd_timer_lines
51
+ end
52
+
53
+ def self.calendar_timer_lines(trigger)
54
+ trigger.on_calendar.flat_map do |spec|
55
+ CalendarSpec.to_on_calendar_values(spec).map { |value| "OnCalendar=#{value}" }
56
+ end
57
+ end
58
+ private_class_method :calendar_timer_lines
59
+
60
+ def self.build_unit(description:, sections:)
61
+ ([
62
+ marker,
63
+ "[Unit]",
64
+ "Description=#{description}",
65
+ ""
66
+ ] + sections).join("\n")
67
+ end
68
+ private_class_method :build_unit
69
+
70
+ def self.marker
71
+ "#{Renderer::MARKER_PREFIX} #{Wheneverd::VERSION}; do not edit."
72
+ end
73
+ private_class_method :marker
74
+ end
75
+ end
76
+ end
@@ -16,7 +16,7 @@ module Wheneverd
16
16
  dest_dir = File.expand_path(unit_dir.to_s)
17
17
  return [] unless Dir.exist?(dest_dir)
18
18
 
19
- deleted = deletable_paths(dest_dir, basename_pattern(identifier))
19
+ deleted = deletable_paths(dest_dir, UnitPathUtils.basename_pattern(identifier))
20
20
  deleted.each { |path| FileUtils.rm_f(path) unless dry_run }
21
21
  deleted
22
22
  end
@@ -27,38 +27,12 @@ module Wheneverd
27
27
 
28
28
  path = File.join(dest_dir, basename)
29
29
  next unless File.file?(path)
30
- next unless generated_marker?(path)
30
+ next unless UnitPathUtils.generated_marker?(path)
31
31
 
32
32
  path
33
33
  end
34
34
  end
35
35
  private_class_method :deletable_paths
36
-
37
- def self.basename_pattern(identifier)
38
- id = sanitize_identifier(identifier)
39
- /\Awheneverd-#{Regexp.escape(id)}-(?:[0-9a-f]{12}(?:-\d+)?|e\d+-j\d+)\.(service|timer)\z/
40
- end
41
- private_class_method :basename_pattern
42
-
43
- def self.generated_marker?(path)
44
- first_line = File.open(path, "r") { |f| f.gets.to_s }
45
- first_line.start_with?(Wheneverd::Systemd::Renderer::MARKER_PREFIX)
46
- end
47
- private_class_method :generated_marker?
48
-
49
- def self.sanitize_identifier(identifier)
50
- raw = identifier.to_s.strip
51
- raise InvalidIdentifierError, "identifier must not be empty" if raw.empty?
52
-
53
- sanitized = raw.gsub(/[^A-Za-z0-9_-]/, "-").gsub(/-+/, "-").gsub(/\A-|-+\z/, "")
54
- if sanitized.empty?
55
- raise InvalidIdentifierError,
56
- "identifier must include at least one alphanumeric character"
57
- end
58
-
59
- sanitized
60
- end
61
- private_class_method :sanitize_identifier
62
36
  end
63
37
  end
64
38
  end
@@ -13,7 +13,7 @@ module Wheneverd
13
13
  dest_dir = File.expand_path(unit_dir.to_s)
14
14
  return [] unless Dir.exist?(dest_dir)
15
15
 
16
- unit_paths(dest_dir, basename_pattern(identifier))
16
+ unit_paths(dest_dir, UnitPathUtils.basename_pattern(identifier))
17
17
  end
18
18
 
19
19
  def self.unit_paths(dest_dir, pattern)
@@ -22,38 +22,12 @@ module Wheneverd
22
22
 
23
23
  path = File.join(dest_dir, basename)
24
24
  next unless File.file?(path)
25
- next unless generated_marker?(path)
25
+ next unless UnitPathUtils.generated_marker?(path)
26
26
 
27
27
  path
28
28
  end
29
29
  end
30
30
  private_class_method :unit_paths
31
-
32
- def self.basename_pattern(identifier)
33
- id = sanitize_identifier(identifier)
34
- /\Awheneverd-#{Regexp.escape(id)}-(?:[0-9a-f]{12}(?:-\d+)?|e\d+-j\d+)\.(service|timer)\z/
35
- end
36
- private_class_method :basename_pattern
37
-
38
- def self.generated_marker?(path)
39
- first_line = File.open(path, "r") { |f| f.gets.to_s }
40
- first_line.start_with?(Wheneverd::Systemd::Renderer::MARKER_PREFIX)
41
- end
42
- private_class_method :generated_marker?
43
-
44
- def self.sanitize_identifier(identifier)
45
- raw = identifier.to_s.strip
46
- raise InvalidIdentifierError, "identifier must not be empty" if raw.empty?
47
-
48
- sanitized = raw.gsub(/[^A-Za-z0-9_-]/, "-").gsub(/-+/, "-").gsub(/\A-|-+\z/, "")
49
- if sanitized.empty?
50
- raise InvalidIdentifierError,
51
- "identifier must include at least one alphanumeric character"
52
- end
53
-
54
- sanitized
55
- end
56
- private_class_method :sanitize_identifier
57
31
  end
58
32
  end
59
33
  end