rakit 0.1.4 → 0.1.6

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.
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "generated/rakit.static_web_server_pb"
5
+
6
+ module Rakit
7
+ # Publish static sites into a configurable root (~/.rakit/wwwroot), regenerate a root index, and control a local HTTP server (start/stop/running).
8
+ # Host and port are configurable (host default 127.0.0.1, port 5099) and returned regardless of server state for URL building.
9
+ # CLI: +rakit static-web-server+ (start|stop|running|publish|view). See specs/003-static-web-server/contracts/ruby-api.md and specs/007-wwwroot-docs-hugo-view/contracts/ruby-api.md.
10
+ module StaticWebServer
11
+ SITE_NAME_REGEX = /\A[a-z0-9\-]+\z/.freeze
12
+
13
+ class << self
14
+ # @return [String] Static root directory (default ~/.rakit/wwwroot). Used by publish and server lifecycle.
15
+ attr_accessor :root
16
+ # @return [String] Host for building URLs (default 127.0.0.1). Returned regardless of server running state.
17
+ attr_accessor :host
18
+ # @return [Integer] Default port for start (default 5099). Used when no override passed to start; returned regardless of server running state.
19
+ attr_accessor :port
20
+ end
21
+
22
+ self.root = ::File.expand_path("~/.rakit/wwwroot")
23
+ self.host = "127.0.0.1"
24
+ self.port = 5099
25
+
26
+ # T005: Validate site name; raise ArgumentError before any filesystem write.
27
+ # Single segment (e.g. "mysite") or multi-segment path (e.g. "louparslow/rakit"); each segment must match SITE_NAME_REGEX.
28
+ def self.validate_site_name!(site_name)
29
+ validate_site_path!(site_name)
30
+ end
31
+
32
+ def self.validate_site_path!(site_name)
33
+ return if site_name.nil? || site_name.to_s.empty?
34
+ site_name.to_s.split(::File::SEPARATOR).each do |seg|
35
+ next if seg.empty?
36
+ unless seg.match?(SITE_NAME_REGEX)
37
+ raise ArgumentError, "site_name segment must be lowercase alphanumeric and dashes only (e.g. my-site); got: #{seg.inspect}"
38
+ end
39
+ end
40
+ end
41
+
42
+ # T006: PID file path for start/stop/running (research: ~/.rakit/static_web_server.pid).
43
+ def self.pid_file_path
44
+ ::File.expand_path("~/.rakit/static_web_server.pid")
45
+ end
46
+
47
+ # T007: Server binary (miniserve) on PATH; used by start. Returns path or nil.
48
+ def self.server_binary
49
+ @server_binary ||= begin
50
+ path = ENV["RAKIT_STATIC_SERVER_BINARY"]
51
+ return path if path && !path.empty?
52
+ which("miniserve")
53
+ end
54
+ end
55
+
56
+ def self.server_binary=(path)
57
+ @server_binary = path
58
+ end
59
+
60
+ # T008: Create root directory if missing; used before publish and start.
61
+ def self.ensure_root
62
+ FileUtils.mkdir_p(root)
63
+ end
64
+
65
+ # Publish static content from source_directory to root/site_name (atomic copy), then regenerate root index.
66
+ # @param site_name [String] Single segment (e.g. "mysite") or path (e.g. "louparslow/rakit"); each segment must match \\A[a-z0-9\-]+\\z.
67
+ # @param source_directory [String] Existing directory path; contents are copied (no traversal outside allowed paths).
68
+ # @return [true] on success.
69
+ # @raise [ArgumentError] for invalid site_name, missing/invalid source_directory, or root not writable.
70
+ def self.publish(site_name, source_directory)
71
+ validate_site_path!(site_name)
72
+ src = ::File.expand_path(source_directory)
73
+ raise ArgumentError, "source_directory does not exist: #{source_directory}" unless ::File.exist?(src)
74
+ raise ArgumentError, "source_directory is not a directory: #{source_directory}" unless ::File.directory?(src)
75
+
76
+ ensure_root
77
+ unless ::File.writable?(root)
78
+ raise ArgumentError, "root directory is not writable: #{root}"
79
+ end
80
+
81
+ target = ::File.join(root, site_name)
82
+ temp_name = ".tmp_#{site_name.to_s.gsub(::File::SEPARATOR, '_')}_#{Process.pid}_#{rand(1_000_000)}"
83
+ temp = ::File.join(root, temp_name)
84
+ FileUtils.mkdir_p(temp)
85
+ FileUtils.cp_r(::File.join(src, "."), temp)
86
+ FileUtils.mkdir_p(::File.dirname(target))
87
+ FileUtils.rm_rf(target)
88
+ FileUtils.mv(temp, target)
89
+ regenerate_root_index
90
+ true
91
+ end
92
+
93
+ # Whether the server process is currently running (PID file + process check). No side effects.
94
+ # @return [Boolean] true if server is running, false otherwise.
95
+ def self.running?
96
+ path = pid_file_path
97
+ return false unless ::File.file?(path)
98
+ pid = ::File.read(path).strip.to_i
99
+ return false if pid <= 0
100
+ Process.getpgid(pid)
101
+ true
102
+ rescue Errno::ESRCH, Errno::EPERM
103
+ false
104
+ end
105
+
106
+ # Start background server serving root. Idempotent: if already running, no-op.
107
+ # @param options [Hash] :port (optional) override default port.
108
+ # @return [true] if server is running (started or already running).
109
+ # @raise [RuntimeError] if port in use or server binary (miniserve) not found.
110
+ def self.start(options = {})
111
+ return true if running?
112
+ ensure_root
113
+ p = options[:port] || port
114
+ raise "port #{p} is already in use; change port or stop the other process" if port_in_use?(p)
115
+ bin = server_binary
116
+ raise "static server binary (miniserve) not found on PATH; install miniserve or set RAKIT_STATIC_SERVER_BINARY" unless bin
117
+ root_path = ::File.expand_path(root)
118
+ pid = spawn(bin, root_path, "--port", p.to_s, out: ::File::NULL, err: ::File::NULL)
119
+ Process.detach(pid)
120
+ FileUtils.mkdir_p(::File.dirname(pid_file_path))
121
+ ::File.write(pid_file_path, pid.to_s)
122
+ true
123
+ end
124
+
125
+ STOP_TIMEOUT = 5
126
+
127
+ # Gracefully terminate server process; remove PID file. Fail-fast if not running.
128
+ # @return [true] if stopped.
129
+ # @raise [RuntimeError] if server was not running (no PID file or process not found).
130
+ def self.stop
131
+ path = pid_file_path
132
+ raise "server is not running (no PID file at #{path})" unless ::File.file?(path)
133
+ pid = ::File.read(path).strip.to_i
134
+ raise "server is not running (invalid PID in #{path})" if pid <= 0
135
+ begin
136
+ Process.getpgid(pid)
137
+ rescue Errno::ESRCH
138
+ ::File.delete(path)
139
+ raise "server is not running (process #{pid} not found)"
140
+ end
141
+ Process.kill(:TERM, pid)
142
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + STOP_TIMEOUT
143
+ while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
144
+ begin
145
+ Process.wait(pid, Process::WNOHANG)
146
+ break
147
+ rescue Errno::ECHILD
148
+ break
149
+ end
150
+ sleep 0.1
151
+ end
152
+ begin
153
+ Process.getpgid(pid)
154
+ Process.kill(:KILL, pid)
155
+ Process.wait(pid)
156
+ rescue Errno::ESRCH, Errno::ECHILD
157
+ # already gone
158
+ end
159
+ ::File.delete(path) if ::File.file?(path)
160
+ true
161
+ end
162
+
163
+ # Open a path on the static server in the default browser. Ensures server is running (starts if not), builds URL, launches browser.
164
+ # @param relative_path [String] URL path (e.g. "/louparslow/rakit/" or "louparslow/rakit"); normalized to one leading slash.
165
+ # @return [true] on success.
166
+ # @raise [RuntimeError] if browser cannot be launched (e.g. headless), with message e.g. "Could not launch browser; display required?"
167
+ def self.view(relative_path)
168
+ start unless running?
169
+ path = relative_path.to_s.strip
170
+ path = "/#{path}" unless path.empty? || path.start_with?("/")
171
+ path = "/" if path.empty?
172
+ url = "http://#{host}:#{port}#{path}"
173
+ launch_browser(url)
174
+ true
175
+ end
176
+
177
+ # T017: Write root/index.html listing all site subdirectories alphabetically with links.
178
+ def self.regenerate_root_index
179
+ entries = Dir.entries(root).sort.select do |e|
180
+ e != "." && e != ".." && e != "index.html" && !e.start_with?(".") && ::File.directory?(::File.join(root, e))
181
+ end
182
+ html = ["<!DOCTYPE html>", "<html><head><meta charset=\"utf-8\"><title>Published sites</title></head><body>", "<h1>Published sites</h1>", "<ul>"]
183
+ entries.each { |name| html << "<li><a href=\"/#{name}/\">#{name}</a></li>" }
184
+ html << "</ul></body></html>"
185
+ ::File.write(::File.join(root, "index.html"), html.join("\n"))
186
+ end
187
+
188
+ class << self
189
+ private
190
+
191
+ def which(cmd)
192
+ exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
193
+ ENV["PATH"].to_s.split(::File::PATH_SEPARATOR).each do |dir|
194
+ next if dir.empty?
195
+ exts.each do |ext|
196
+ exe = ::File.join(dir, "#{cmd}#{ext}")
197
+ return exe if ::File.executable?(exe) && !::File.directory?(exe)
198
+ end
199
+ end
200
+ nil
201
+ end
202
+
203
+ def port_in_use?(p)
204
+ require "socket"
205
+ TCPServer.new("127.0.0.1", p).close
206
+ false
207
+ rescue Errno::EADDRINUSE
208
+ true
209
+ end
210
+
211
+ def launch_browser(url)
212
+ cmd = case ::RbConfig::CONFIG["host_os"]
213
+ when /darwin|mac os/i
214
+ ["open", url]
215
+ when /mswin|mingw|windows/i
216
+ ["cmd", "/c", "start", "", url]
217
+ else
218
+ ["xdg-open", url]
219
+ end
220
+ system(*cmd, out: ::File::NULL, err: ::File::NULL)
221
+ return true if $?.success?
222
+ raise "Could not launch browser; display required?"
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "set"
5
+ require "generated/rakit.word_count_pb"
6
+
7
+ module Rakit
8
+ # Schema-driven token frequency counting over JSON content (keys or values).
9
+ # CLI: +rakit word-count+ (--json-keys). See specs/004-word-count/contracts/ruby-api.md.
10
+ module WordCount
11
+ class << self
12
+ # @param request [Rakit::Generated::WordCountRequest]
13
+ # @return [Rakit::Generated::WordCountResult]
14
+ def count(request)
15
+ json_str = resolve_json_input(request)
16
+ return json_str if json_str.is_a?(Rakit::Generated::WordCountResult)
17
+
18
+ data = JSON.parse(json_str)
19
+ config = request.config || Rakit::Generated::WordCountConfig.new
20
+ keys = extract_keys(data)
21
+ tokens = keys.flat_map { |k| normalize_and_split(k, config) }
22
+ counts = filter_count_sort(tokens, config)
23
+ total = tokens.size
24
+ unique = counts.size
25
+
26
+ Rakit::Generated::WordCountResult.new(
27
+ success: true,
28
+ message: "",
29
+ counts: counts.map { |tok, cnt| Rakit::Generated::TokenCount.new(token: tok, count: cnt) },
30
+ exit_code: 0,
31
+ stderr: "",
32
+ total_tokens: total,
33
+ unique_tokens: unique
34
+ )
35
+ rescue JSON::ParserError => e
36
+ Rakit::Generated::WordCountResult.new(
37
+ success: false,
38
+ message: e.message,
39
+ exit_code: 1,
40
+ stderr: e.message
41
+ )
42
+ rescue Errno::ENOENT, Errno::EACCES => e
43
+ Rakit::Generated::WordCountResult.new(
44
+ success: false,
45
+ message: e.message,
46
+ exit_code: 1,
47
+ stderr: e.message
48
+ )
49
+ end
50
+
51
+ # T004: Recursive key extraction from JSON structure. Returns array of key strings (duplicates preserved).
52
+ def extract_keys(obj)
53
+ case obj
54
+ when Hash
55
+ obj.keys.map(&:to_s) + obj.values.flat_map { |v| extract_keys(v) }
56
+ when Array
57
+ obj.flat_map { |v| extract_keys(v) }
58
+ else
59
+ []
60
+ end
61
+ end
62
+
63
+ # T005: Normalize and split one key into tokens. Pipeline: case -> snake/kebab -> camelCase.
64
+ def normalize_and_split(token_string, config)
65
+ return [] if token_string.nil? || !token_string.is_a?(String)
66
+
67
+ s = token_string.dup
68
+ s = s.downcase if config.case_insensitive
69
+ parts = [s]
70
+ parts = parts.flat_map { |p| p.split(/_|-/) } if config.split_snake_kebab
71
+ parts = parts.flat_map { |p| p.split(/(?=[A-Z])/).reject(&:empty?) } if config.split_camel_case
72
+ parts.map(&:downcase).reject(&:empty?)
73
+ end
74
+
75
+ # T006: Filter (min_length, stopwords), count, sort (count desc, token asc), top_n.
76
+ def filter_count_sort(tokens, config)
77
+ min_len = config.min_token_length || 0
78
+ stopwords = config.stopwords || []
79
+ normalized_stop = config.case_insensitive ? stopwords.map(&:downcase).to_set : stopwords.to_set
80
+
81
+ filtered = tokens.select do |t|
82
+ compare_t = config.case_insensitive ? t.downcase : t
83
+ t.length >= min_len && !normalized_stop.include?(compare_t)
84
+ end
85
+ freq = filtered.tally
86
+ sorted = freq.sort_by { |token, count| [-count, token] }
87
+ top_n = config.top_n || 0
88
+ top_n.positive? ? sorted.take(top_n) : sorted
89
+ end
90
+
91
+ private
92
+
93
+ def resolve_json_input(request)
94
+ json = request.json
95
+ return error_result("No JSON source", 1) unless json
96
+
97
+ if json.json_file && !json.json_file.empty?
98
+ path = json.json_file
99
+ return error_result("File not found: #{path}", 1) unless ::File.file?(path)
100
+ return error_result("File not readable: #{path}", 1) unless ::File.readable?(path)
101
+ return ::File.read(path)
102
+ end
103
+
104
+ if json.inline_json && !json.inline_json.empty?
105
+ return json.inline_json
106
+ end
107
+
108
+ # stdin: caller must set inline_json with the read content (per contracts/ruby-api.md)
109
+ error_result("No JSON source (set json_file or inline_json)", 1)
110
+ end
111
+
112
+ def error_result(message, exit_code)
113
+ Rakit::Generated::WordCountResult.new(
114
+ success: false,
115
+ message: message,
116
+ exit_code: exit_code,
117
+ stderr: message
118
+ )
119
+ end
120
+ end
121
+ end
122
+ end
data/lib/rakit.rb CHANGED
@@ -22,9 +22,12 @@ require_relative "rakit/gem"
22
22
  require_relative "rakit/git"
23
23
  require_relative "rakit/task"
24
24
  require_relative "rakit/protobuf"
25
- # Defer loading so rake tasks that don't need Shell/Data (e.g. clobber) work without google-protobuf.
25
+ # Defer loading so rake tasks that don't need Shell (e.g. clobber) work without google-protobuf.
26
26
  autoload :Shell, "rakit/shell"
27
- autoload :Data, "rakit/data"
27
+ require_relative "rakit/static_web_server"
28
+ require_relative "rakit/hugo"
29
+ require_relative "rakit/word_count"
30
+ require_relative "rakit/file"
28
31
  require_relative "rakit/azure/dev_ops"
29
32
 
30
33
  def run(cmd)
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rakit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - rakit
8
- bindir: bin
8
+ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
@@ -41,16 +41,22 @@ dependencies:
41
41
  name: google-protobuf
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - "~>"
44
+ - - ">="
45
45
  - !ruby/object:Gem::Version
46
46
  version: '3.25'
47
+ - - "<"
48
+ - !ruby/object:Gem::Version
49
+ version: '6'
47
50
  type: :runtime
48
51
  prerelease: false
49
52
  version_requirements: !ruby/object:Gem::Requirement
50
53
  requirements:
51
- - - "~>"
54
+ - - ">="
52
55
  - !ruby/object:Gem::Version
53
56
  version: '3.25'
57
+ - - "<"
58
+ - !ruby/object:Gem::Version
59
+ version: '6'
54
60
  - !ruby/object:Gem::Dependency
55
61
  name: rubyzip
56
62
  requirement: !ruby/object:Gem::Requirement
@@ -93,22 +99,32 @@ dependencies:
93
99
  - - "~>"
94
100
  - !ruby/object:Gem::Version
95
101
  version: '5.0'
96
- executables: []
102
+ executables:
103
+ - rakit
97
104
  extensions: []
98
105
  extra_rdoc_files: []
99
106
  files:
100
- - lib/generated/azure.devops_pb.rb
107
+ - exe/rakit
101
108
  - lib/generated/data_pb.rb
102
- - lib/generated/example_pb.rb
103
- - lib/generated/shell_pb.rb
109
+ - lib/generated/rakit.azure_pb.rb
110
+ - lib/generated/rakit.example_pb.rb
111
+ - lib/generated/rakit.file_pb.rb
112
+ - lib/generated/rakit.shell_pb.rb
113
+ - lib/generated/rakit.static_web_server_pb.rb
114
+ - lib/generated/rakit.word_count_pb.rb
104
115
  - lib/rakit.rb
105
116
  - lib/rakit/azure/dev_ops.rb
106
- - lib/rakit/data.rb
117
+ - lib/rakit/cli/file.rb
118
+ - lib/rakit/cli/word_count.rb
119
+ - lib/rakit/file.rb
107
120
  - lib/rakit/gem.rb
108
121
  - lib/rakit/git.rb
122
+ - lib/rakit/hugo.rb
109
123
  - lib/rakit/protobuf.rb
110
124
  - lib/rakit/shell.rb
125
+ - lib/rakit/static_web_server.rb
111
126
  - lib/rakit/task.rb
127
+ - lib/rakit/word_count.rb
112
128
  homepage: https://gitlab.com/gems/rakit
113
129
  licenses:
114
130
  - MIT
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
- # Generated by the protocol buffer compiler. DO NOT EDIT!
3
- # source: azure.devops.proto
4
-
5
- require "google/protobuf"
6
-
7
- descriptor_data = "\n\x12\x61zure.devops.proto\x12\x0brakit.azure\"L\n\x08Pipeline\x12\x0b\n\x03org\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0bpipeline_id\x18\x03 \x01(\x05\x12\r\n\x05token\x18\x04 \x01(\t\"R\n\x18GetPipelineResultRequest\x12\'\n\x08pipeline\x18\x01 \x01(\x0b\x32\x15.rakit.azure.Pipeline\x12\r\n\x05token\x18\x02 \x01(\t\"C\n\x0ePipelineStatus\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0e\n\x06\x65rrors\x18\x02 \x03(\t\x12\x10\n\x08warnings\x18\x03 \x03(\t\"8\n\x0ePipelineResult\x12&\n\x04runs\x18\x01 \x03(\x0b\x32\x18.rakit.azure.PipelineRun\"\x97\x01\n\x0bPipelineRun\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05state\x18\x03 \x01(\t\x12\x0e\n\x06result\x18\x04 \x01(\t\x12\x14\n\x0c\x63reated_date\x18\x05 \x01(\t\x12\x15\n\rfinished_date\x18\x06 \x01(\t\x12\"\n\x06stages\x18\x07 \x03(\x0b\x32\x12.rakit.azure.Stage\"&\n\x05Issue\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t\"G\n\x03Job\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06result\x18\x02 \x01(\t\x12\"\n\x06issues\x18\x03 \x03(\x0b\x32\x12.rakit.azure.Issue\"i\n\x05Stage\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06result\x18\x02 \x01(\t\x12\x1e\n\x04jobs\x18\x03 \x03(\x0b\x32\x10.rakit.azure.Job\x12\"\n\x06issues\x18\x04 \x03(\x0b\x32\x12.rakit.azure.Issue\"`\n\x0eTimelineRecord\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0e\n\x06result\x18\x03 \x01(\t\x12\"\n\x06issues\x18\x04 \x03(\x0b\x32\x12.rakit.azure.Issue\"\xa8\x01\n\x14PipelineResultDetail\x12\x12\n\nsuccessful\x18\x01 \x01(\x08\x12\x0e\n\x06\x65rrors\x18\x02 \x01(\t\x12\x10\n\x08warnings\x18\x03 \x01(\t\x12%\n\x03run\x18\x04 \x01(\x0b\x32\x18.rakit.azure.PipelineRun\x12\x33\n\x0e\x66\x61iled_records\x18\x05 \x03(\x0b\x32\x1b.rakit.azure.TimelineRecord2i\n\x0ePipelineServer\x12W\n\x11GetPipelineResult\x12%.rakit.azure.GetPipelineResultRequest\x1a\x1b.rakit.azure.PipelineResultB\x0f\xea\x02\x0cRakit::Azureb\x06proto3"
8
-
9
- pool = ::Google::Protobuf::DescriptorPool.generated_pool
10
- pool.add_serialized_file(descriptor_data)
11
-
12
- module Rakit
13
- module Azure
14
- Pipeline = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.Pipeline").msgclass
15
- GetPipelineResultRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.GetPipelineResultRequest").msgclass
16
- PipelineStatus = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.PipelineStatus").msgclass
17
- PipelineResult = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.PipelineResult").msgclass
18
- PipelineRun = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.PipelineRun").msgclass
19
- Issue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.Issue").msgclass
20
- Job = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.Job").msgclass
21
- Stage = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.Stage").msgclass
22
- TimelineRecord = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.TimelineRecord").msgclass
23
- PipelineResultDetail = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.PipelineResultDetail").msgclass
24
- end
25
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
- # Generated by the protocol buffer compiler. DO NOT EDIT!
3
- # source: shell.proto
4
-
5
- require 'google/protobuf'
6
-
7
-
8
- descriptor_data = "\n\x0bshell.proto\x12\x0brakit.shell\"\x9a\x02\n\x07\x43ommand\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x61rgs\x18\x02 \x03(\t\x12\x19\n\x11working_directory\x18\x03 \x01(\t\x12\x17\n\x0ftimeout_seconds\x18\x04 \x01(\x05\x12\x1a\n\x12\x65xpected_exit_code\x18\x05 \x01(\x05\x12\x17\n\x0f\x65xpected_stdout\x18\x06 \x01(\t\x12\x17\n\x0f\x65xpected_stderr\x18\x07 \x01(\t\x12<\n\x13\x61\x63\x63\x65ptance_criteria\x18\x08 \x03(\x0b\x32\x1f.rakit.shell.AcceptanceCriteria\x12\x13\n\x0b\x65xit_status\x18\t \x01(\x05\x12\x0e\n\x06stdout\x18\n \x01(\t\x12\x0e\n\x06stderr\x18\x0b \x01(\t\"1\n\x12\x41\x63\x63\x65ptanceCriteria\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"-\n\nTestResult\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0e\n\x06\x65rrors\x18\x02 \x03(\t\"b\n\rFormatRequest\x12%\n\x07\x63ommand\x18\x01 \x01(\x0b\x32\x14.rakit.shell.Command\x12*\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x1a.rakit.shell.CommandFormat\" \n\x0e\x46ormatResponse\x12\x0e\n\x06output\x18\x01 \x01(\t*Z\n\rCommandFormat\x12\x1e\n\x1a\x43OMMAND_FORMAT_UNSPECIFIED\x10\x00\x12\x0c\n\x08ONE_LINE\x10\x01\x12\x0e\n\nMULTI_LINE\x10\x02\x12\x0b\n\x07\x43OMPACT\x10\x03\x32\xc1\x01\n\x0e\x43ommandService\x12\x35\n\x07\x45xecute\x12\x14.rakit.shell.Command\x1a\x14.rakit.shell.Command\x12\x35\n\x04Test\x12\x14.rakit.shell.Command\x1a\x17.rakit.shell.TestResult\x12\x41\n\x06\x46ormat\x12\x1a.rakit.shell.FormatRequest\x1a\x1b.rakit.shell.FormatResponseB\x0f\xea\x02\x0cRakit::Shellb\x06proto3"
9
-
10
- pool = ::Google::Protobuf::DescriptorPool.generated_pool
11
- pool.add_serialized_file(descriptor_data)
12
-
13
- module Rakit
14
- module Shell
15
- Command = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.shell.Command").msgclass
16
- AcceptanceCriteria = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.shell.AcceptanceCriteria").msgclass
17
- TestResult = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.shell.TestResult").msgclass
18
- FormatRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.shell.FormatRequest").msgclass
19
- FormatResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.shell.FormatResponse").msgclass
20
- CommandFormat = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.shell.CommandFormat").enummodule
21
- end
22
- end
data/lib/rakit/data.rb DELETED
@@ -1,173 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
-
5
- require "generated/data_pb"
6
-
7
- module Rakit
8
- module Data
9
- # Export all stored messages from DataService.data_dir to export_dir in the given format.
10
- # If there are no .pb files under data_dir, no files are created.
11
- #
12
- # @param export_dir [String] target directory (created if needed)
13
- # @param export_format [Rakit::Data::ExportFormat] PROTOBUF_BINARY_FILES (mirror .pb layout),
14
- # PROTOBUF_JSON_FILES (same layout with .json), or PROTOBUF_BINARY_ZIPPED (single data.zip)
15
- # @return [void]
16
- # @raise [ArgumentError] if export_format is not a supported ExportFormat value
17
- def self.export(export_dir, export_format)
18
- base = DataService.data_dir
19
- export_dir = File.expand_path(export_dir)
20
- FileUtils.mkdir_p(export_dir)
21
-
22
- pb_paths = Dir.glob(File.join(base, "**", "*.pb"))
23
- return if pb_paths.empty?
24
-
25
- case export_format
26
- when ExportFormat::PROTOBUF_BINARY_FILES
27
- _export_binary_files(pb_paths, base, export_dir)
28
- when ExportFormat::PROTOBUF_JSON_FILES
29
- _export_json_files(pb_paths, base, export_dir)
30
- when ExportFormat::PROTOBUF_BINARY_ZIPPED
31
- _export_binary_zipped(pb_paths, base, export_dir)
32
- else
33
- raise ArgumentError, "unsupported export_format: #{export_format.inspect}"
34
- end
35
- end
36
-
37
- def self._rel_parts(base, path)
38
- rel = path[(base.end_with?(File::SEPARATOR) ? base.length : base.length + 1)..]
39
- parts = rel.split(File::SEPARATOR)
40
- type_name = parts[0..-2].join("::")
41
- unique_name = File.basename(parts[-1], ".pb")
42
- [type_name, unique_name, rel]
43
- end
44
-
45
- def self._export_binary_files(pb_paths, base, export_dir)
46
- pb_paths.each do |path|
47
- type_name, _unique_name, rel = _rel_parts(base, path)
48
- message = DataService.load(type_name, File.basename(path, ".pb"))
49
- out_path = File.join(export_dir, rel)
50
- FileUtils.mkdir_p(File.dirname(out_path))
51
- File.binwrite(out_path, message.class.encode(message))
52
- end
53
- end
54
-
55
- def self._export_json_files(pb_paths, base, export_dir)
56
- pb_paths.each do |path|
57
- type_name, unique_name, rel = _rel_parts(base, path)
58
- message = DataService.load(type_name, unique_name)
59
- out_rel = rel.sub(/\.pb\z/, ".json")
60
- out_path = File.join(export_dir, out_rel)
61
- FileUtils.mkdir_p(File.dirname(out_path))
62
- File.write(out_path, message.class.encode_json(message))
63
- end
64
- end
65
-
66
- def self._export_binary_zipped(pb_paths, base, export_dir)
67
- require "zip"
68
- zip_path = File.join(export_dir, "data.zip")
69
- FileUtils.rm_f(zip_path)
70
- Zip::File.open(zip_path, Zip::File::CREATE) do |zip|
71
- pb_paths.each do |path|
72
- type_name, unique_name, rel = _rel_parts(base, path)
73
- message = DataService.load(type_name, unique_name)
74
- zip.get_output_stream(rel) { |io| io.write(message.class.encode(message)) }
75
- end
76
- end
77
- end
78
-
79
- # Persist and retrieve protobuf message instances under a configurable data root.
80
- #
81
- # Storage layout: root is {data_dir} (default +~/.rakit/data+). Path for a message is
82
- # +data_dir/TYPE_PATH/unique_name.pb+. TYPE_PATH is the Ruby class name with +::+ replaced by
83
- # +File::SEPARATOR+ (e.g. +Rakit::Shell::Command+ → +Rakit/Shell/Command+). File content is
84
- # binary protobuf; no character encoding.
85
- module DataService
86
- # @return [String] current data root (default: expanded +~/.rakit/data+)
87
- def self.data_dir
88
- @data_dir ||= File.expand_path("~/.rakit/data")
89
- end
90
-
91
- # @param path [String] set the data root for subsequent operations (e.g. tests use a temp dir)
92
- # @return [void]
93
- def self.data_dir=(path)
94
- @data_dir = path
95
- end
96
-
97
- # Store a proto message under a unique name.
98
- # @param message [Object] instance of a generated protobuf message class
99
- # @param unique_name [String] non-empty, must not contain path separators or +..+
100
- # @return [void]
101
- # @raise [ArgumentError] if unique_name is empty/blank or contains path traversal
102
- # @raise [Errno::EACCES] etc. on permission failure
103
- def self.store(message, unique_name)
104
- _validate_unique_name!(unique_name)
105
- klass = message.class
106
- path = _path(klass.name, unique_name)
107
- FileUtils.mkdir_p(File.dirname(path))
108
- File.binwrite(path, klass.encode(message))
109
- end
110
-
111
- # Load a stored message.
112
- # @param type_name [String] Ruby class name (e.g. +"Rakit::Shell::Command"+)
113
- # @param unique_name [String] same rules as for store
114
- # @return [Object] decoded message instance
115
- # @raise [ArgumentError] if unique_name invalid
116
- # @raise [NameError] if type_name is not a valid constant
117
- # @raise [Errno::ENOENT] if the file does not exist
118
- def self.load(type_name, unique_name)
119
- _validate_unique_name!(unique_name)
120
- klass = Object.const_get(type_name.to_s)
121
- path = _path(klass.name, unique_name.to_s)
122
- raise Errno::ENOENT, path unless File.file?(path)
123
-
124
- klass.decode(File.binread(path))
125
- end
126
-
127
- # Remove a stored message by type and unique name (no-op if file absent).
128
- # @param type_name [String] Ruby class name
129
- # @param unique_name [String] same rules as for store
130
- # @return [void]
131
- # @raise [ArgumentError] if unique_name invalid
132
- def self.remove(type_name, unique_name)
133
- _validate_unique_name!(unique_name)
134
- path = _path(type_name.to_s, unique_name.to_s)
135
- File.delete(path) if File.file?(path)
136
- end
137
-
138
- # Return unique names (without +.pb+) for the given type.
139
- # @param type_name [String] Ruby class name for directory resolution
140
- # @return [Array<String>] empty if directory missing or no .pb files
141
- # @raise [NameError] if type_name is not a valid constant
142
- # @raise [ArgumentError] if type_name yields empty path segments (e.g. from _dir_for_type)
143
- def self.get_names(type_name)
144
- dir = _dir_for_type(type_name.to_s)
145
- return [] unless File.directory?(dir)
146
-
147
- Dir.children(dir).select { |f| File.file?(File.join(dir, f)) && f.end_with?(".pb") }.map { |f| f.chomp(".pb") }
148
- end
149
-
150
- def self._validate_unique_name!(unique_name)
151
- u = unique_name.to_s
152
- raise ArgumentError, "unique_name must be a non-empty string" if u.strip.empty?
153
- if u.include?("/") || u.include?("\\") || u.include?("..")
154
- raise ArgumentError, "unique_name must not contain path separators or '..'"
155
- end
156
- end
157
-
158
- def self._path(type_name, unique_name)
159
- dir = _dir_for_type(type_name)
160
- File.join(dir, "#{unique_name}.pb")
161
- end
162
-
163
- # PACKAGE_PATH/MESSAGE_NAME: e.g. Rakit::Shell::Command -> Rakit/Shell/Command
164
- def self._dir_for_type(type_name)
165
- parts = type_name.split("::")
166
- raise ArgumentError, "type_name must be a qualified constant path" if parts.empty?
167
-
168
- relative = parts.join(File::SEPARATOR)
169
- File.join(data_dir, relative)
170
- end
171
- end
172
- end
173
- end