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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c4d813e1e0ad0b44e738d4ba42767b5fb490dcaf5a7883328c91faa2ab726305
4
- data.tar.gz: 1185929834d9f12adb4afd8a1bc2d1a4a366d99b260afe1d075a1ca58dc6dc26
3
+ metadata.gz: ff946dbc0ba39d3e90ecf3cbed4c9ea81c1818fcd1d206f00cf6763dfe05ebe1
4
+ data.tar.gz: cd42856421c97365ff63547252ea4e8ba8a5f17dc822eea8af67b45ce8861b8f
5
5
  SHA512:
6
- metadata.gz: ed43947461d6e8dadf5825c8075e08816535c42202a9764c7b57d1fb4a7948a0dbfe1ff57e13cc710a853efa6910f802b93774b239736ed30bcec145812c08fc
7
- data.tar.gz: 4406adc4f70afde1953cd8ea82979d2c37bf6b096232930724f2537b04d5ae187437fafa29215163ebd985eb132b89c7ee990497ba2cdb70617166b5d88114ee
6
+ metadata.gz: a4d31fc560c96e1b10b7b42cb0c32e4aa222823e64bde6a17cacb891246d67caefb9e42b5f71ce23a26b14392c33eb819f42fc3e199a0b2f10a0f040bb5e5193
7
+ data.tar.gz: fc2e7a43ed41af1b7b00cb4cc8a228054071cec67ca53bb81829134ab884f8a6b5730796a699bdd6066262d3fee18ff0234f535d2fe967b5d26543d2d7b2097e
data/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ On release, entries are moved into `## x.y.z` sections that match the gem versio
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.4.0
9
+
10
+ - Docs: adds a copy/paste "deploy a simple schedule" example and refines README status section.
11
+ - Refactor: extracts `UnitPathUtils` module for shared identifier/path utilities across `UnitWriter`, `UnitDeleter`, `UnitLister`, and `Renderer`.
12
+ - Refactor: adds polymorphic `Trigger::Base` interface with `#systemd_timer_lines` and `#signature` methods for all trigger types.
13
+ - Refactor: splits `CronParser` into focused `FieldParser` and `DowParser` submodules for maintainability.
14
+ - Refactor: implements strategy pattern for `PeriodParser` with dedicated strategies for Duration, String, Symbol, and Array inputs.
15
+ - Refactor: extracts `UnitContentBuilder` from `Renderer` for cleaner separation of unit content generation.
16
+ - Refactor: adds `Validation` module with composable validators (`type`, `positive_integer`, `non_empty_string`, `non_empty_array`, `in_range`).
17
+
8
18
  ## 0.3.0
9
19
 
10
20
  - Schedule DSL: `command` accepts argv arrays, adds a `shell` helper for `/bin/bash -lc`, and `wheneverd init` includes examples.
data/README.md CHANGED
@@ -4,14 +4,23 @@ Wheneverd is to systemd timers what the [`whenever` gem](https://github.com/java
4
4
 
5
5
  ## Status
6
6
 
7
- Pre-1.0, but working end-to-end for user systemd timers:
7
+ Pre-1.0, but working end-to-end for systemd user timers on Linux:
8
8
 
9
9
  - Loads a Ruby schedule DSL file (default: `config/schedule.rb`).
10
10
  - Renders systemd `.service`/`.timer` units (interval, calendar, and 5-field cron schedules).
11
- - Writes, lists, and deletes generated unit files (default: `~/.config/systemd/user`).
11
+ - Writes, diffs, shows, and deletes generated unit files (default: `~/.config/systemd/user`).
12
12
  - Enables/starts/stops/disables/restarts timers via `systemctl --user`.
13
+ - Validates `OnCalendar=` values with `systemd-analyze` (optional unit verification).
13
14
  - Manages lingering via `loginctl` (so timers can run while logged out).
14
15
 
16
+ Non-goals / not yet implemented:
17
+
18
+ - System-level units (`/etc/systemd/system`) / `systemctl` without `--user`.
19
+ - Non-systemd schedulers (cron, launchd, etc).
20
+ - Non-Linux platforms (no Windows/macOS support).
21
+
22
+ Expect the CLI and generated unit details to change until 1.0.
23
+
15
24
  See `FEATURE_SUMMARY.md` for user-visible behavior, and `CHANGELOG.md` for release notes.
16
25
 
17
26
  ## Installation
@@ -62,6 +71,40 @@ every 1.day, at: "4:30 am" do
62
71
  end
63
72
  ```
64
73
 
74
+ ### Deploy a simple schedule (copy/paste)
75
+
76
+ From your project root (the default identifier is the current directory name):
77
+
78
+ ```bash
79
+ # Install (skip if already in your Gemfile)
80
+ bundle add wheneverd
81
+ bundle install
82
+
83
+ # Write a schedule that appends a timestamp to ~/.cache/wheneverd-demo.log every minute
84
+ mkdir -p config
85
+ cat > config/schedule.rb <<'RUBY'
86
+ # frozen_string_literal: true
87
+
88
+ every "1m" do
89
+ shell "mkdir -p ~/.cache && date >> ~/.cache/wheneverd-demo.log"
90
+ end
91
+ RUBY
92
+
93
+ # Preview, write units, and enable/start the timer(s)
94
+ bundle exec wheneverd show
95
+ bundle exec wheneverd validate
96
+ bundle exec wheneverd write
97
+ bundle exec wheneverd activate
98
+
99
+ # Verify it’s installed and running
100
+ bundle exec wheneverd status
101
+ tail -n 5 ~/.cache/wheneverd-demo.log
102
+
103
+ # Stop/disable timers and remove generated unit files
104
+ bundle exec wheneverd deactivate
105
+ bundle exec wheneverd delete
106
+ ```
107
+
65
108
  Preview the generated units:
66
109
 
67
110
  ```bash
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "calendar_symbol_period_list"
3
+ require_relative "period_strategy"
4
4
 
5
5
  module Wheneverd
6
6
  module DSL
@@ -8,24 +8,12 @@ module Wheneverd
8
8
  #
9
9
  # Supported period forms are described in the README.
10
10
  #
11
- # Notes:
12
- #
13
- # - Interval strings and {Wheneverd::Duration} values produce monotonic triggers
14
- # ({Wheneverd::Trigger::Interval}).
15
- # - Calendar symbol periods and cron strings produce calendar triggers
16
- # ({Wheneverd::Trigger::Calendar}).
17
- # - The `at:` option is only valid for calendar triggers, with a convenience exception for
18
- # `every 1.day, at: ...` which is treated as a daily calendar trigger.
11
+ # Uses a strategy pattern to delegate parsing to specialized strategy classes:
12
+ # - {PeriodStrategy::DurationStrategy} for Duration values
13
+ # - {PeriodStrategy::StringStrategy} for interval strings and cron expressions
14
+ # - {PeriodStrategy::SymbolStrategy} for calendar symbols
15
+ # - {PeriodStrategy::ArrayStrategy} for arrays of calendar symbols
19
16
  class PeriodParser
20
- DAY_SECONDS = 60 * 60 * 24
21
- REBOOT_NOT_SUPPORTED_MESSAGE =
22
- "The :reboot period is not supported; use an interval or calendar period instead"
23
-
24
- CALENDAR_SYMBOLS = %i[
25
- hour day month year weekday weekend
26
- monday tuesday wednesday thursday friday saturday sunday
27
- ].freeze
28
-
29
17
  attr_reader :path
30
18
 
31
19
  # @param path [String] schedule path for error reporting
@@ -38,98 +26,11 @@ module Wheneverd
38
26
  # @return [Wheneverd::Trigger::Interval, Wheneverd::Trigger::Calendar]
39
27
  def trigger_for(period, at:)
40
28
  at_times = AtNormalizer.normalize(at, path: path)
41
- trigger_for_period(period, at_times: at_times)
29
+ strategy = PeriodStrategy.for(period, path: path)
30
+ strategy.parse(period, at_times: at_times)
42
31
  rescue Wheneverd::InvalidIntervalError => e
43
32
  raise InvalidPeriodError.new(e.message, path: path)
44
33
  end
45
-
46
- private
47
-
48
- def trigger_for_period(period, at_times:)
49
- return duration_trigger_for(period, at_times: at_times) if period.is_a?(Wheneverd::Duration)
50
- return array_trigger_for(period, at_times: at_times) if period.is_a?(Array)
51
- return string_trigger_for(period, at_times: at_times) if period.is_a?(String)
52
- return symbol_trigger_for(period, at_times: at_times) if period.is_a?(Symbol)
53
-
54
- raise InvalidPeriodError.new("Unsupported period type: #{period.class}", path: path)
55
- end
56
-
57
- def duration_trigger_for(duration, at_times:)
58
- if at_times.any?
59
- return daily_calendar_trigger(at_times) if duration.to_i == DAY_SECONDS
60
-
61
- raise InvalidPeriodError.new("at: is only supported with calendar periods", path: path)
62
- end
63
-
64
- Wheneverd::Trigger::Interval.new(seconds: duration.to_i)
65
- end
66
-
67
- def daily_calendar_trigger(at_times)
68
- Wheneverd::Trigger::Calendar.new(on_calendar: build_calendar_specs("day", at_times))
69
- end
70
-
71
- def string_trigger_for(str, at_times:)
72
- period_str = str.strip
73
-
74
- return interval_trigger(period_str, at_times: at_times) if interval_string?(period_str)
75
-
76
- return cron_trigger(period_str, at_times: at_times) if cron_string?(period_str)
77
-
78
- raise InvalidPeriodError.new("Unrecognized period #{period_str.inspect}", path: path)
79
- end
80
-
81
- def interval_trigger(period_str, at_times:)
82
- if at_times.any?
83
- raise InvalidPeriodError.new("at: is not supported for interval periods", path: path)
84
- end
85
-
86
- seconds = Wheneverd::Interval.parse(period_str)
87
- Wheneverd::Trigger::Interval.new(seconds: seconds)
88
- end
89
-
90
- def cron_trigger(period_str, at_times:)
91
- if at_times.any?
92
- raise InvalidPeriodError.new("at: is not supported for cron periods", path: path)
93
- end
94
-
95
- Wheneverd::Trigger::Calendar.new(on_calendar: ["cron:#{period_str}"])
96
- end
97
-
98
- def symbol_trigger_for(sym, at_times:)
99
- raise InvalidPeriodError.new(REBOOT_NOT_SUPPORTED_MESSAGE, path: path) if sym == :reboot
100
-
101
- if CALENDAR_SYMBOLS.include?(sym)
102
- return Wheneverd::Trigger::Calendar.new(
103
- on_calendar: build_calendar_specs(sym.to_s, at_times)
104
- )
105
- end
106
-
107
- raise InvalidPeriodError.new("Unknown period symbol: #{sym.inspect}", path: path)
108
- end
109
-
110
- def array_trigger_for(periods, at_times:)
111
- bases = CalendarSymbolPeriodList.validate(
112
- periods,
113
- allowed_symbols: CALENDAR_SYMBOLS,
114
- path: path
115
- ).map(&:to_s)
116
- specs = bases.flat_map { |base| build_calendar_specs(base, at_times) }.uniq
117
- Wheneverd::Trigger::Calendar.new(on_calendar: specs)
118
- end
119
-
120
- def build_calendar_specs(base, at_times)
121
- return [base] if at_times.empty?
122
-
123
- at_times.map { |t| "#{base}@#{t}" }
124
- end
125
-
126
- def interval_string?(str)
127
- /\A-?\d+[smhdw]\z/.match?(str)
128
- end
129
-
130
- def cron_string?(str)
131
- str.split(/\s+/).length == 5
132
- end
133
34
  end
134
35
  end
135
36
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../calendar_symbol_period_list"
4
+
5
+ module Wheneverd
6
+ module DSL
7
+ module PeriodStrategy
8
+ # Strategy for parsing Array period values.
9
+ #
10
+ # Handles arrays of calendar symbols like [:monday, :wednesday, :friday].
11
+ class ArrayStrategy < Base
12
+ def handles?(period)
13
+ period.is_a?(Array)
14
+ end
15
+
16
+ def parse(periods, at_times:)
17
+ bases = CalendarSymbolPeriodList.validate(
18
+ periods,
19
+ allowed_symbols: CALENDAR_SYMBOLS,
20
+ path: path
21
+ ).map(&:to_s)
22
+
23
+ specs = bases.flat_map { |base| build_calendar_specs(base, at_times) }.uniq
24
+ calendar_trigger(on_calendar: specs)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wheneverd
4
+ module DSL
5
+ module PeriodStrategy
6
+ # Base class for period parsing strategies.
7
+ #
8
+ # Each strategy handles a specific type of period value (Duration, String, Symbol, Array)
9
+ # and converts it into a trigger object.
10
+ class Base
11
+ DAY_SECONDS = 60 * 60 * 24
12
+
13
+ CALENDAR_SYMBOLS = %i[
14
+ hour day month year weekday weekend
15
+ monday tuesday wednesday thursday friday saturday sunday
16
+ ].freeze
17
+
18
+ attr_reader :path
19
+
20
+ # @param path [String] schedule path for error reporting
21
+ def initialize(path:)
22
+ @path = path
23
+ end
24
+
25
+ # Check if this strategy handles the given period type.
26
+ #
27
+ # @param period [Object] the period value
28
+ # @return [Boolean]
29
+ def handles?(_period)
30
+ raise NotImplementedError, "#{self.class} must implement #handles?"
31
+ end
32
+
33
+ # Parse the period into a trigger.
34
+ #
35
+ # @param period [Object] the period value
36
+ # @param at_times [Array<String>] normalized at: times
37
+ # @return [Wheneverd::Trigger::Interval, Wheneverd::Trigger::Calendar]
38
+ # @raise [Wheneverd::DSL::InvalidPeriodError]
39
+ def parse(_period, at_times:)
40
+ raise NotImplementedError, "#{self.class} must implement #parse"
41
+ end
42
+
43
+ protected
44
+
45
+ def raise_period_error(message)
46
+ raise InvalidPeriodError.new(message, path: path)
47
+ end
48
+
49
+ def build_calendar_specs(base, at_times)
50
+ return [base] if at_times.empty?
51
+
52
+ at_times.map { |t| "#{base}@#{t}" }
53
+ end
54
+
55
+ def calendar_trigger(on_calendar:)
56
+ Wheneverd::Trigger::Calendar.new(on_calendar: on_calendar)
57
+ end
58
+
59
+ def interval_trigger(seconds:)
60
+ Wheneverd::Trigger::Interval.new(seconds: seconds)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wheneverd
4
+ module DSL
5
+ module PeriodStrategy
6
+ # Strategy for parsing Duration period values.
7
+ #
8
+ # Duration values produce Interval triggers, except for 1.day with at: times
9
+ # which produces a Calendar trigger for daily scheduling.
10
+ class DurationStrategy < Base
11
+ def handles?(period)
12
+ period.is_a?(Wheneverd::Duration)
13
+ end
14
+
15
+ def parse(duration, at_times:)
16
+ if at_times.any?
17
+ return daily_calendar_trigger(at_times) if duration.to_i == DAY_SECONDS
18
+
19
+ raise_period_error("at: is only supported with calendar periods")
20
+ end
21
+
22
+ interval_trigger(seconds: duration.to_i)
23
+ end
24
+
25
+ private
26
+
27
+ def daily_calendar_trigger(at_times)
28
+ calendar_trigger(on_calendar: build_calendar_specs("day", at_times))
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wheneverd
4
+ module DSL
5
+ module PeriodStrategy
6
+ # Strategy for parsing String period values.
7
+ #
8
+ # Handles interval strings (e.g., "5m", "1h") and cron expressions.
9
+ class StringStrategy < Base
10
+ INTERVAL_PATTERN = /\A-?\d+[smhdw]\z/.freeze
11
+
12
+ def handles?(period)
13
+ period.is_a?(String)
14
+ end
15
+
16
+ def parse(str, at_times:)
17
+ period_str = str.strip
18
+
19
+ return parse_interval(period_str, at_times: at_times) if interval_string?(period_str)
20
+
21
+ return parse_cron(period_str, at_times: at_times) if cron_string?(period_str)
22
+
23
+ raise_period_error("Unrecognized period #{period_str.inspect}")
24
+ end
25
+
26
+ private
27
+
28
+ def parse_interval(period_str, at_times:)
29
+ raise_period_error("at: is not supported for interval periods") if at_times.any?
30
+
31
+ seconds = Wheneverd::Interval.parse(period_str)
32
+ interval_trigger(seconds: seconds)
33
+ end
34
+
35
+ def parse_cron(period_str, at_times:)
36
+ raise_period_error("at: is not supported for cron periods") if at_times.any?
37
+
38
+ calendar_trigger(on_calendar: ["cron:#{period_str}"])
39
+ end
40
+
41
+ def interval_string?(str)
42
+ INTERVAL_PATTERN.match?(str)
43
+ end
44
+
45
+ def cron_string?(str)
46
+ str.split(/\s+/).length == 5
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wheneverd
4
+ module DSL
5
+ module PeriodStrategy
6
+ # Strategy for parsing Symbol period values.
7
+ #
8
+ # Handles calendar symbols like :day, :monday, :weekend, etc.
9
+ class SymbolStrategy < Base
10
+ REBOOT_NOT_SUPPORTED_MESSAGE =
11
+ "The :reboot period is not supported; use an interval or calendar period instead"
12
+
13
+ def handles?(period)
14
+ period.is_a?(Symbol)
15
+ end
16
+
17
+ def parse(sym, at_times:)
18
+ raise_period_error(REBOOT_NOT_SUPPORTED_MESSAGE) if sym == :reboot
19
+
20
+ if CALENDAR_SYMBOLS.include?(sym)
21
+ return calendar_trigger(
22
+ on_calendar: build_calendar_specs(sym.to_s, at_times)
23
+ )
24
+ end
25
+
26
+ raise_period_error("Unknown period symbol: #{sym.inspect}")
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "period_strategy/base"
4
+ require_relative "period_strategy/duration_strategy"
5
+ require_relative "period_strategy/string_strategy"
6
+ require_relative "period_strategy/symbol_strategy"
7
+ require_relative "period_strategy/array_strategy"
8
+
9
+ module Wheneverd
10
+ module DSL
11
+ # Period parsing strategies for converting DSL period values into triggers.
12
+ #
13
+ # Each strategy handles a specific type of period value and converts it
14
+ # into the appropriate trigger type.
15
+ module PeriodStrategy
16
+ # Default strategies in order of precedence.
17
+ DEFAULT_STRATEGIES = [
18
+ DurationStrategy,
19
+ ArrayStrategy,
20
+ StringStrategy,
21
+ SymbolStrategy
22
+ ].freeze
23
+
24
+ # Find the strategy that handles the given period type.
25
+ #
26
+ # @param period [Object] the period value
27
+ # @param path [String] schedule path for error reporting
28
+ # @return [Base] the strategy instance
29
+ # @raise [Wheneverd::DSL::InvalidPeriodError] if no strategy handles the period
30
+ def self.for(period, path:)
31
+ strategy_class = DEFAULT_STRATEGIES.find do |klass|
32
+ klass.new(path: path).handles?(period)
33
+ end
34
+
35
+ if strategy_class.nil?
36
+ raise InvalidPeriodError.new("Unsupported period type: #{period.class}", path: path)
37
+ end
38
+
39
+ strategy_class.new(path: path)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -11,13 +11,7 @@ module Wheneverd
11
11
 
12
12
  # @param seconds [Integer] duration in seconds (must be positive)
13
13
  def initialize(seconds)
14
- unless seconds.is_a?(Integer)
15
- raise ArgumentError, "Duration seconds must be an Integer (got #{seconds.class})"
16
- end
17
-
18
- raise ArgumentError, "Duration seconds must be positive (got #{seconds})" if seconds <= 0
19
-
20
- @seconds = seconds
14
+ @seconds = Validation.positive_integer(seconds, name: "Duration seconds")
21
15
  end
22
16
 
23
17
  def to_i
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wheneverd
4
+ # Base error class for wheneverd exceptions.
5
+ class Error < StandardError; end
6
+
4
7
  # Raised when {Wheneverd::Interval.parse} cannot parse or validate an interval string.
5
8
  class InvalidIntervalError < Error; end
6
9
 
@@ -21,17 +21,32 @@ module Wheneverd
21
21
  # @return [Integer] seconds
22
22
  # @raise [Wheneverd::InvalidIntervalError] if the input is invalid
23
23
  def self.parse(str)
24
- input = str.to_s.strip
24
+ input = normalize_input(str)
25
+ match = parse_match(input)
26
+ n = parse_number(match[:n], input)
27
+ n * MULTIPLIERS.fetch(match[:unit])
28
+ end
29
+
30
+ def self.normalize_input(str)
31
+ str.to_s.strip
32
+ end
33
+ private_class_method :normalize_input
34
+
35
+ def self.parse_match(input)
25
36
  match = FORMAT.match(input)
26
- unless match
27
- raise InvalidIntervalError,
28
- "Invalid interval #{input.inspect}; expected <n>s|m|h|d|w (example: \"5m\")"
29
- end
37
+ return match if match
38
+
39
+ raise InvalidIntervalError,
40
+ "Invalid interval #{input.inspect}; expected <n>s|m|h|d|w (example: \"5m\")"
41
+ end
42
+ private_class_method :parse_match
30
43
 
31
- n = Integer(match[:n], 10)
44
+ def self.parse_number(number_str, input)
45
+ n = Integer(number_str, 10)
32
46
  raise InvalidIntervalError, "Interval must be positive (got #{input.inspect})" if n <= 0
33
47
 
34
- n * MULTIPLIERS.fetch(match[:unit])
48
+ n
35
49
  end
50
+ private_class_method :parse_number
36
51
  end
37
52
  end