Class: Kabosu::DictManager

Inherits:
Object
  • Object
show all
Defined in:
lib/kabosu/dict_manager.rb

Defined Under Namespace

Classes: DictNotFound, DownloadError

Constant Summary collapse

EDITIONS =
%w[small core full].freeze
EDITION_PRIORITY =
%w[full core small].freeze
GITHUB_REPO =
"WorksApplications/SudachiDict"
GITHUB_API =
"https://api.github.com"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(dir: self.class.default_dir) ⇒ DictManager

Returns a new instance of DictManager.



24
25
26
# File 'lib/kabosu/dict_manager.rb', line 24

def initialize(dir: self.class.default_dir)
  @dir = dir
end

Instance Attribute Details

#dirObject (readonly)

Returns the value of attribute dir.



28
29
30
# File 'lib/kabosu/dict_manager.rb', line 28

def dir
  @dir
end

Class Method Details

.default_dirObject

Default storage directory. Honors KABOSU_DICT_DIR so consumers can point the gem at a Docker volume / shared mount without subclassing or threading ‘dir:` through every call site. Falls back to ~/.kabosu/dict/.



20
21
22
# File 'lib/kabosu/dict_manager.rb', line 20

def self.default_dir
  ENV["KABOSU_DICT_DIR"] || File.join(Dir.home, ".kabosu", "dict")
end

Instance Method Details

#available_versionsObject

List available versions from GitHub releases.



168
169
170
171
172
# File 'lib/kabosu/dict_manager.rb', line 168

def available_versions
  uri = URI("#{GITHUB_API}/repos/#{GITHUB_REPO}/releases")
  response = http_get(uri, headers: { "Accept" => "application/json" })
  JSON.parse(response.body).map { |r| r["tag_name"].sub(/\Av/, "") }
end

#find(edition: nil) ⇒ Object

Find the best available dictionary path. Prefers: latest version, then full > core > small.

Raises:



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/kabosu/dict_manager.rb', line 108

def find(edition: nil)
  candidates = installed
  raise DictNotFound, "No dictionaries installed. Run: rake kabosu:install" if candidates.empty?

  if edition
    edition = validate_edition(edition)
    match = candidates.find { |d| d[:edition] == edition }
    raise DictNotFound, "No #{edition} dictionary installed" unless match
    return match[:path]
  end

  # Group by version (already sorted newest-first), pick best edition
  by_version = candidates.group_by { |d| d[:version] }
  latest_version_dicts = by_version.values.first

  best = EDITION_PRIORITY.each do |ed|
    found = latest_version_dicts.find { |d| d[:edition] == ed }
    break found if found
  end

  best.is_a?(Hash) ? best[:path] : latest_version_dicts.first[:path]
end

#install(edition = "core", version: nil) ⇒ Object

Download and extract a dictionary edition.

manager.install("small")
manager.install("core", version: "20260116")


37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/kabosu/dict_manager.rb', line 37

def install(edition = "core", version: nil)
  edition = validate_edition(edition)
  version ||= latest_version

  dest_dir = File.join(@dir, "sudachi-dictionary-#{version}")
  dic_path = File.join(dest_dir, "system_#{edition}.dic")

  if File.exist?(dic_path)
    $stderr.puts "Already installed: #{dic_path}"
    return dic_path
  end

  url = release_asset_url(version, edition)
  zip_path = File.join(@dir, "sudachi-dictionary-#{version}-#{edition}.zip")

  FileUtils.mkdir_p(@dir)
  download(url, zip_path)
  extract(zip_path, @dir)
  FileUtils.rm_f(zip_path)

  unless File.exist?(dic_path)
    raise DownloadError, "Expected #{dic_path} after extraction, but file not found"
  end

  $stderr.puts "Installed: #{dic_path}"
  dic_path
end

#install_if_missing(edition = "core", version: nil) ⇒ Object

Idempotent install. Returns the existing dictionary path if a matching one is already on disk; otherwise downloads and extracts. Useful for entrypoint scripts and CI hooks that should converge on the desired state without paying the network cost on every run.

manager.install_if_missing("core")
manager.install_if_missing("core", version: "20260116")


73
74
75
76
77
78
79
80
81
# File 'lib/kabosu/dict_manager.rb', line 73

def install_if_missing(edition = "core", version: nil)
  edition = validate_edition(edition)
  matching = installed.find do |d|
    d[:edition] == edition && (version.nil? || d[:version] == version)
  end
  return matching[:path] if matching

  install(edition, version: version)
end

#installedObject

List all installed dictionaries. Returns an array of hashes: { version:, edition:, path: }



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/kabosu/dict_manager.rb', line 87

def installed
  results = []
  return results unless Dir.exist?(@dir)

  Dir.glob(File.join(@dir, "sudachi-dictionary-*")).sort.reverse.each do |version_dir|
    next unless File.directory?(version_dir)

    version = File.basename(version_dir).sub("sudachi-dictionary-", "")
    EDITIONS.each do |edition|
      dic = File.join(version_dir, "system_#{edition}.dic")
      next unless File.exist?(dic)

      results << { version: version, edition: edition, path: dic }
    end
  end

  results
end

#latest_versionObject

Fetch the latest release tag from GitHub.



158
159
160
161
162
163
164
165
# File 'lib/kabosu/dict_manager.rb', line 158

def latest_version
  uri = URI("#{GITHUB_API}/repos/#{GITHUB_REPO}/releases/latest")
  response = http_get(uri, headers: { "Accept" => "application/json" })
  data = JSON.parse(response.body)
  tag = data["tag_name"]
  # Tags are like "v20260116" — strip the "v" prefix
  tag.sub(/\Av/, "")
end

#remove(edition: nil, version: nil) ⇒ Object

Remove a specific dictionary edition, or an entire version.

Raises:



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/kabosu/dict_manager.rb', line 134

def remove(edition: nil, version: nil)
  targets = installed
  targets = targets.select { |d| d[:version] == version } if version
  targets = targets.select { |d| d[:edition] == edition } if edition

  raise DictNotFound, "No matching dictionary found" if targets.empty?

  targets.each do |d|
    FileUtils.rm_f(d[:path])
    $stderr.puts "Removed: #{d[:path]}"

    # Clean up empty version directories
    version_dir = File.dirname(d[:path])
    dics_remaining = Dir.glob(File.join(version_dir, "system_*.dic"))
    if dics_remaining.empty?
      FileUtils.rm_rf(version_dir)
      $stderr.puts "Removed empty directory: #{version_dir}"
    end
  end
end