Class: Prdigest::Config

Inherits:
Object
  • Object
show all
Defined in:
lib/prdigest/config.rb

Constant Summary collapse

REPOSITORY_SEGMENT =
/\A[A-Za-z0-9_.-]+\z/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(raw, path: nil) ⇒ Config

Returns a new instance of Config.



53
54
55
56
# File 'lib/prdigest/config.rb', line 53

def initialize(raw, path: nil)
  @raw = raw
  @path = path
end

Instance Attribute Details

#pathObject (readonly)

Returns the value of attribute path.



9
10
11
# File 'lib/prdigest/config.rb', line 9

def path
  @path
end

#rawObject (readonly)

Returns the value of attribute raw.



9
10
11
# File 'lib/prdigest/config.rb', line 9

def raw
  @raw
end

Class Method Details

.load(path) ⇒ Object



23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/prdigest/config.rb', line 23

def self.load(path)
  path = File.expand_path(path)
  raise ConfigError, "config not found: #{path}" unless File.file?(path)

  raw = YAML.safe_load_file(path, aliases: true)
  raise ConfigError, "config root must be a mapping" unless raw.is_a?(Hash)

  new(raw, path: path).tap(&:validate!)
rescue ConfigError
  raise
rescue StandardError => e
  raise ConfigError, "cannot load config: #{e.class}"
end

.normalize_repos(values, label: "repository") ⇒ Object

Raises:



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/prdigest/config.rb', line 37

def self.normalize_repos(values, label: "repository")
  normalized = Array(values).map { |value| value.to_s.strip }
  raise ConfigError, "#{label} must list at least one owner/name" if normalized.empty?

  invalid = normalized.find do |repository|
    parts = repository.split("/", -1)
    parts.length != 2 ||
      parts.any? { |part| part.empty? || %w[. ..].include?(part) || !part.match?(REPOSITORY_SEGMENT) }
  end
  raise ConfigError, "#{label} entry must be owner/name; got #{invalid.inspect}" if invalid

  normalized.each_with_object({}) do |repository, unique|
    unique[repository.downcase] ||= repository
  end.values.freeze
end

.resolve_path(explicit: nil, env: ENV, system_path: "/etc/prdigest/config.yml") ⇒ Object

Raises:



13
14
15
16
17
18
19
20
21
# File 'lib/prdigest/config.rb', line 13

def self.resolve_path(explicit: nil, env: ENV, system_path: "/etc/prdigest/config.yml")
  return File.expand_path(explicit) if explicit && !explicit.to_s.empty?

  from_env = env["PRDIGEST_CONFIG"].to_s
  return File.expand_path(from_env) unless from_env.empty?
  return system_path if File.file?(system_path)

  raise ConfigError, "config path is required (--config, PRDIGEST_CONFIG, or /etc/prdigest/config.yml)"
end

Instance Method Details

#chat_idObject



76
77
78
79
80
# File 'lib/prdigest/config.rb', line 76

def chat_id
  Integer(raw.dig("telegram", "chat_id"))
rescue TypeError, ArgumentError
  nil
end

#chat_id_allowlistObject



82
83
84
85
86
# File 'lib/prdigest/config.rb', line 82

def chat_id_allowlist
  Array(raw.dig("telegram", "chat_id_allowlist")).map { |id| Integer(id) }
rescue TypeError, ArgumentError
  []
end

#delivery_state_pathObject



104
105
106
# File 'lib/prdigest/config.rb', line 104

def delivery_state_path
  raw.dig("state", "delivery_path") || File.join(File.dirname(state_path), "deliveries")
end

#empty_messageObject



96
97
98
# File 'lib/prdigest/config.rb', line 96

def empty_message
  raw.dig("digest", "empty_message") || "Merged PR digest — {date}\nTotal: 0 PRs"
end

#github_token(env = ENV) ⇒ Object



66
67
68
69
# File 'lib/prdigest/config.rb', line 66

def github_token(env = ENV)
  env_name = raw.dig("github", "token_env") || "GITHUB_TOKEN"
  env[env_name.to_s].to_s
end

#line_stats?Boolean

Returns:

  • (Boolean)


88
89
90
# File 'lib/prdigest/config.rb', line 88

def line_stats?
  raw.dig("digest", "line_stats") != false
end

#max_catchup_daysObject



112
113
114
115
116
# File 'lib/prdigest/config.rb', line 112

def max_catchup_days
  Integer(raw.dig("schedule", "max_catchup_days") || 7)
rescue TypeError, ArgumentError
  raise ConfigError, "schedule.max_catchup_days must be an integer from 1 to 30"
end

#reposObject



62
63
64
# File 'lib/prdigest/config.rb', line 62

def repos
  self.class.normalize_repos(raw.dig("github", "repos"), label: "github.repos")
end

#schedule_cronObject



108
109
110
# File 'lib/prdigest/config.rb', line 108

def schedule_cron
  raw.dig("schedule", "cron") || "5 9 * * *"
end

#send_empty?Boolean

Returns:

  • (Boolean)


92
93
94
# File 'lib/prdigest/config.rb', line 92

def send_empty?
  raw.dig("digest", "send_empty") != false
end

#state_pathObject



100
101
102
# File 'lib/prdigest/config.rb', line 100

def state_path
  raw.dig("state", "path") || File.expand_path("~/.local/share/prdigest/state.json")
end

#telegram_token(env = ENV) ⇒ Object



71
72
73
74
# File 'lib/prdigest/config.rb', line 71

def telegram_token(env = ENV)
  env_name = raw.dig("telegram", "token_env") || "TELEGRAM_BOT_TOKEN"
  env[env_name.to_s].to_s
end

#timezoneObject



58
59
60
# File 'lib/prdigest/config.rb', line 58

def timezone
  raw.fetch("timezone", "UTC").to_s
end

#validate!Object

Raises:



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/prdigest/config.rb', line 118

def validate!
  begin
    TZInfo::Timezone.get(timezone)
  rescue TZInfo::InvalidTimezoneIdentifier
    raise ConfigError, "timezone must be a resolvable IANA identifier"
  end
  unless (1..30).cover?(max_catchup_days)
    raise ConfigError, "schedule.max_catchup_days must be from 1 to 30"
  end
  repos
  raise ConfigError, "telegram.chat_id is required" if chat_id.nil?
  raise ConfigError, "telegram.chat_id_allowlist must not be empty" if chat_id_allowlist.empty?
  unless chat_id_allowlist.include?(chat_id)
    raise ConfigError, "telegram.chat_id must be present in chat_id_allowlist"
  end
  self
end