Module: Rakit::WordCloud
- Defined in:
- lib/rakit/word_cloud.rb
Class Attribute Summary collapse
-
.auto_install ⇒ Object
Returns the value of attribute auto_install.
-
.wcloud_path ⇒ Object
Returns the value of attribute wcloud_path.
-
.working_directory ⇒ Object
Returns the value of attribute working_directory.
Class Method Summary collapse
- .build_wcloud_argv(request, out_path, mask_path, font_path, exclude_path) ⇒ Object
- .fail_result(output_image, message, exit_code) ⇒ Object
-
.generate(request, stdin_content: nil) ⇒ Object
Generate word cloud.
-
.install(config = {}) ⇒ Object
Install: run cargo install wcloud.
-
.path_safe?(path, base_dir) ⇒ Boolean
Validate path has no traversal.
-
.resolve_cargo(_config = {}) ⇒ Object
Resolve cargo: which cargo.
- .resolve_text_content(text_source, base_dir, stdin_content) ⇒ Object
-
.resolve_wcloud(config = {}) ⇒ Object
Resolve wcloud: config path, then ENV RAKIT_WCLOUD_BINARY, then which wcloud.
-
.run_wcloud(wcloud_bin, argv, stdin: nil) ⇒ Object
Run wcloud with argv (no shell).
-
.status(config = {}) ⇒ Object
Status: resolve wcloud and cargo; no side effects.
-
.validate_optional_file_path(path, base_dir, label) ⇒ Object
Validate optional file path (mask, font, exclude-words): if present, must exist and be readable.
-
.validate_output_path(path, base_dir) ⇒ Object
Validate output image path: parent creatable/writable, no traversal.
-
.validate_text_file_path(path, base_dir) ⇒ Object
Validate text file path: exists, readable file, no traversal.
- .which(cmd) ⇒ Object
Class Attribute Details
.auto_install ⇒ Object
Returns the value of attribute auto_install.
10 11 12 |
# File 'lib/rakit/word_cloud.rb', line 10 def auto_install @auto_install end |
.wcloud_path ⇒ Object
Returns the value of attribute wcloud_path.
10 11 12 |
# File 'lib/rakit/word_cloud.rb', line 10 def wcloud_path @wcloud_path end |
.working_directory ⇒ Object
Returns the value of attribute working_directory.
10 11 12 |
# File 'lib/rakit/word_cloud.rb', line 10 def working_directory @working_directory end |
Class Method Details
.build_wcloud_argv(request, out_path, mask_path, font_path, exclude_path) ⇒ Object
225 226 227 228 229 230 231 232 233 234 235 |
# File 'lib/rakit/word_cloud.rb', line 225 def self.build_wcloud_argv(request, out_path, mask_path, font_path, exclude_path) argv = ["-o", out_path] argv += ["--width", request.width.to_s] if request.width && request.width > 0 argv += ["--height", request.height.to_s] if request.height && request.height > 0 argv += ["--seed", request.rng_seed.to_s] if request.rng_seed && request.rng_seed >= 0 argv += ["--mask", mask_path] if mask_path != "" argv += ["--font", font_path] if font_path != "" argv += ["--exclude-words", exclude_path] if exclude_path != "" argv += ["--max-words", request.max_words.to_s] if request.max_words && request.max_words > 0 argv end |
.fail_result(output_image, message, exit_code) ⇒ Object
189 190 191 192 193 194 195 196 197 198 |
# File 'lib/rakit/word_cloud.rb', line 189 def self.fail_result(output_image, , exit_code) Rakit::Generated::GenerateResult.new( success: false, message: .to_s, output_image: output_image.to_s, exit_code: exit_code, stdout: "", stderr: .to_s ) end |
.generate(request, stdin_content: nil) ⇒ Object
Generate word cloud. request is GenerateRequest. Optional stdin_content when text source is stdin. Returns GenerateResult. Does not leave partial output on validation or tool failure.
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/rakit/word_cloud.rb', line 125 def self.generate(request, stdin_content: nil) cfg = request.config || Rakit::Generated::WordCloudConfig.new base_dir = (cfg.working_directory.to_s.strip != "") ? cfg.working_directory : (self.working_directory || Dir.pwd) # Resolve text content and validate non-empty content, err = resolve_text_content(request.text, base_dir, stdin_content) unless content return fail_result(request.output_image.to_s, err, -1) end # Validate output path (creates parent if missing) ok, out_path = validate_output_path(request.output_image.to_s, base_dir) unless ok return fail_result(request.output_image.to_s, out_path, -1) end # Optional file paths (mask, font, exclude-words) mask_path = request.mask_image.to_s.strip if mask_path != "" ok, err = validate_optional_file_path(mask_path, base_dir, "Mask image") return fail_result(out_path, err, -1) unless ok end font_path = request.font_file.to_s.strip if font_path != "" ok, err = validate_optional_file_path(font_path, base_dir, "Font file") return fail_result(out_path, err, -1) unless ok end exclude_path = request.exclude_words_file.to_s.strip if exclude_path != "" ok, err = validate_optional_file_path(exclude_path, base_dir, "Exclude-words file") return fail_result(out_path, err, -1) unless ok end config_hash = { wcloud_path: cfg.wcloud_path.to_s, auto_install: cfg.auto_install } wcloud_bin = resolve_wcloud(config_hash) if wcloud_bin.nil? && cfg.auto_install install_result = install(config_hash) unless install_result.success return fail_result(out_path, "wcloud not found and auto-install failed: #{install_result.}", -1) end wcloud_bin = resolve_wcloud(config_hash) end unless wcloud_bin return fail_result(out_path, "wcloud not found. Install with: cargo install wcloud", -1) end argv = build_wcloud_argv(request, out_path, mask_path, font_path, exclude_path) stdout, stderr, exit_code = run_wcloud(wcloud_bin, argv, stdin: content) result = Rakit::Generated::GenerateResult.new( success: exit_code == 0, message: exit_code == 0 ? "OK" : stderr.to_s.strip, output_image: out_path, exit_code: exit_code, stdout: stdout.to_s, stderr: stderr.to_s, wcloud_resolved_path: wcloud_bin ) unless result.success ::File.unlink(out_path) if ::File.exist?(out_path) end result end |
.install(config = {}) ⇒ Object
Install: run cargo install wcloud. Returns GenerateResult with success, message, stderr.
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/rakit/word_cloud.rb', line 94 def self.install(config = {}) cfg = config.is_a?(Hash) ? config : {} cargo_bin = resolve_cargo(cfg) unless cargo_bin return fail_result("", "cargo not found. Install Rust from https://rustup.rs/ then run: cargo install wcloud", -1) end _stdout, stderr, status = Open3.capture3(cargo_bin, "install", "wcloud") Rakit::Generated::GenerateResult.new( success: status.success?, message: status.success? ? "wcloud installed or already present" : stderr.to_s.strip, output_image: "", exit_code: status.exitstatus, stdout: "", stderr: stderr.to_s ) end |
.path_safe?(path, base_dir) ⇒ Boolean
Validate path has no traversal. If relative, must be under base_dir; absolute paths allowed.
16 17 18 19 20 21 22 23 24 25 |
# File 'lib/rakit/word_cloud.rb', line 16 def self.path_safe?(path, base_dir) return false if path.nil? || path.to_s.strip.empty? p = path.to_s.strip return false if p.include?("..") base = ::File.(base_dir.to_s) = ::File.(p, base) # Absolute path: allow (no traversal) return true if p.start_with?("/") || (p.size >= 2 && p[1] == ":") == base || .start_with?(base + ::File::SEPARATOR) end |
.resolve_cargo(_config = {}) ⇒ Object
Resolve cargo: which cargo.
75 76 77 |
# File 'lib/rakit/word_cloud.rb', line 75 def self.resolve_cargo(_config = {}) which("cargo") end |
.resolve_text_content(text_source, base_dir, stdin_content) ⇒ Object
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/rakit/word_cloud.rb', line 200 def self.resolve_text_content(text_source, base_dir, stdin_content) return [nil, "Text source is empty; provide non-empty text."] if text_source.nil? case text_source.source when :text_file path = text_source.text_file.to_s.strip return [nil, "Text file path is empty."] if path.empty? ok, = validate_text_file_path(path, base_dir) return [nil, ] unless ok content = ::File.read() return [nil, "Text source is empty; provide non-empty text."] if content.to_s.strip.empty? return [content, nil] when :inline_text content = text_source.inline_text.to_s return [nil, "Text source is empty; provide non-empty text."] if content.strip.empty? return [content, nil] when :stdin content = stdin_content.to_s return [nil, "Text source is empty; provide non-empty text."] if content.strip.empty? return [content, nil] else [nil, "Invalid text source."] end end |
.resolve_wcloud(config = {}) ⇒ Object
Resolve wcloud: config path, then ENV RAKIT_WCLOUD_BINARY, then which wcloud.
66 67 68 69 70 71 72 |
# File 'lib/rakit/word_cloud.rb', line 66 def self.resolve_wcloud(config = {}) path = config[:wcloud_path] || config["wcloud_path"] || self.wcloud_path return path if path && path.to_s.strip != "" && ::File.executable?(::File.(path)) env_path = ENV["RAKIT_WCLOUD_BINARY"] return env_path if env_path && env_path.to_s.strip != "" && ::File.executable?(::File.(env_path)) which("wcloud") end |
.run_wcloud(wcloud_bin, argv, stdin: nil) ⇒ Object
Run wcloud with argv (no shell). Optional stdin: string. Returns [stdout, stderr, exit_status].
238 239 240 241 242 243 244 245 |
# File 'lib/rakit/word_cloud.rb', line 238 def self.run_wcloud(wcloud_bin, argv, stdin: nil) if stdin stdout, stderr, status = Open3.capture3(wcloud_bin, *argv, stdin_data: stdin) else stdout, stderr, status = Open3.capture3(wcloud_bin, *argv) end [stdout.to_s, stderr.to_s, status.exitstatus] end |
.status(config = {}) ⇒ Object
Status: resolve wcloud and cargo; no side effects. Returns ToolStatus.
80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/rakit/word_cloud.rb', line 80 def self.status(config = {}) cfg = config.is_a?(Hash) ? config : {} wcloud_path = cfg[:wcloud_path] || cfg["wcloud_path"] || self.wcloud_path wcloud_bin = resolve_wcloud(cfg) cargo_bin = resolve_cargo(cfg) Rakit::Generated::ToolStatus.new( wcloud_found: !wcloud_bin.nil?, wcloud_path: wcloud_bin.to_s, cargo_found: !cargo_bin.nil?, cargo_path: cargo_bin.to_s ) end |
.validate_optional_file_path(path, base_dir, label) ⇒ Object
Validate optional file path (mask, font, exclude-words): if present, must exist and be readable.
54 55 56 57 58 59 60 61 62 63 |
# File 'lib/rakit/word_cloud.rb', line 54 def self.validate_optional_file_path(path, base_dir, label) return [true, nil] if path.nil? || path.to_s.strip.empty? base = ::File.(base_dir.to_s) = ::File.(path.to_s, base) return [false, "#{label}: path contains traversal or is outside working directory"] unless path_safe?(path, base_dir) return [false, "#{label} file does not exist: #{path}"] unless ::File.exist?() return [false, "#{label} not a file: #{path}"] unless ::File.file?() return [false, "#{label} not readable: #{path}"] unless ::File.readable?() [true, ] end |
.validate_output_path(path, base_dir) ⇒ Object
Validate output image path: parent creatable/writable, no traversal.
39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/rakit/word_cloud.rb', line 39 def self.validate_output_path(path, base_dir) base = ::File.(base_dir.to_s) = ::File.(path.to_s, base) return [false, "Path contains traversal (..) or is outside working directory"] unless path_safe?(path, base_dir) parent = ::File.dirname() begin FileUtils.mkdir_p(parent) unless ::File.directory?(parent) rescue SystemCallError => e return [false, "Cannot create output directory: #{e.}"] end return [false, "Output path not writable: #{path}"] unless ::File.writable?(parent) [true, ] end |
.validate_text_file_path(path, base_dir) ⇒ Object
Validate text file path: exists, readable file, no traversal. base_dir = working_directory or Dir.pwd.
28 29 30 31 32 33 34 35 36 |
# File 'lib/rakit/word_cloud.rb', line 28 def self.validate_text_file_path(path, base_dir) base = ::File.(base_dir.to_s) = ::File.(path.to_s, base) return [false, "Path contains traversal (..) or is outside working directory"] unless path_safe?(path, base_dir) return [false, "File does not exist: #{path}"] unless ::File.exist?() return [false, "Not a file: #{path}"] unless ::File.file?() return [false, "File not readable: #{path}"] unless ::File.readable?() [true, ] end |
.which(cmd) ⇒ Object
111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/rakit/word_cloud.rb', line 111 def self.which(cmd) exe = nil ENV["PATH"].to_s.split(::File::PATH_SEPARATOR).each do |dir| full = ::File.join(dir.strip, cmd) if ::File.executable?(full) exe = full break end end exe end |