Class: RepoTender::State::Store

Inherits:
Object
  • Object
show all
Defined in:
lib/repo_tender/state/store.rb

Overview

Machine-managed state at $XDG_STATE_HOME/repo-tender/state.yaml. Never hand-edited (per PRD ยง3.2). Per-repo + per-org records with a fixed status enum; the store validates the enum and timestamp format on write.

Defined Under Namespace

Classes: Org, Repo, State

Constant Summary collapse

STATUSES =
%w[clean dirty diverged detached wrong_branch missing error].freeze

Class Method Summary collapse

Class Method Details

.build_state(raw) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/repo_tender/state/store.rb', line 104

def self.build_state(raw)
  repos = (raw["repos"] || {}).each_with_object({}) do |(key, attrs), acc|
    acc[key] = Repo.new(
      default_branch: attrs["default_branch"],
      last_fetch_at: attrs["last_fetch_at"],
      last_synced_at: attrs["last_synced_at"],
      status: attrs["status"],
      last_error: attrs["last_error"]
    )
  end
  orgs = (raw["orgs"] || {}).each_with_object({}) do |(key, attrs), acc|
    acc[key] = Org.new(
      last_listed_at: attrs["last_listed_at"],
      repo_count: attrs["repo_count"] || 0,
      last_error: attrs["last_error"]
    )
  end
  State.new(repos: repos, orgs: orgs)
end

.emit(state) ⇒ Object



131
132
133
134
135
136
137
# File 'lib/repo_tender/state/store.rb', line 131

def self.emit(state)
  payload = {
    "repos" => state.repos.each_with_object({}) { |(k, v), acc| acc[k] = v.to_h_compact },
    "orgs" => state.orgs.each_with_object({}) { |(k, v), acc| acc[k] = v.to_h_compact }
  }
  YAML.dump(payload, line_width: -1)
end

.load(path) ⇒ Object



67
68
69
70
# File 'lib/repo_tender/state/store.rb', line 67

def self.load(path)
  raw = read_yaml(path)
  Success(build_state(raw))
end

.read_yaml(path) ⇒ Object



96
97
98
99
100
101
102
# File 'lib/repo_tender/state/store.rb', line 96

def self.read_yaml(path)
  return {} unless File.exist?(path)
  # Time class permitted because state.yaml stores ISO8601
  # timestamps; Psych will deserialize them as Time when the
  # scalar is tagged. No other arbitrary classes allowed.
  YAML.safe_load_file(path, permitted_classes: [Symbol, Time], aliases: false) || {}
end

.validate(state) ⇒ Object



87
88
89
90
91
92
93
94
# File 'lib/repo_tender/state/store.rb', line 87

def self.validate(state)
  state.repos.each do |key, repo|
    unless STATUSES.include?(repo.status)
      return Failure({repos: {key => {status: ["must be one of: #{STATUSES.join(", ")}"]}}})
    end
  end
  Success(state)
end

.write(path, state) ⇒ Object



72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/repo_tender/state/store.rb', line 72

def self.write(path, state)
  validation = validate(state)
  return validation if validation.failure?

  FileUtils.mkdir_p(File.dirname(path))
  tmp = "#{path}.tmp.#{Process.pid}"
  begin
    File.write(tmp, emit(state))
    File.rename(tmp, path)
  ensure
    File.delete(tmp) if File.exist?(tmp)
  end
  Success(state)
end