rakit 0.1.4 → 0.1.5
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.
- checksums.yaml +4 -4
- data/exe/rakit +83 -0
- data/lib/generated/azure.devops_pb.rb +2 -1
- data/lib/generated/data_pb.rb +1 -2
- data/lib/generated/example_pb.rb +2 -1
- data/lib/generated/rakit.file_pb.rb +22 -0
- data/lib/generated/rakit.word_count_pb.rb +22 -0
- data/lib/generated/static_web_server_pb.rb +20 -0
- data/lib/rakit/cli/word_count.rb +131 -0
- data/lib/rakit/gem.rb +8 -8
- data/lib/rakit/git.rb +10 -4
- data/lib/rakit/protobuf.rb +11 -9
- data/lib/rakit/static_web_server.rb +182 -0
- data/lib/rakit/word_count.rb +122 -0
- data/lib/rakit.rb +4 -2
- metadata +11 -4
- data/lib/rakit/data.rb +0 -173
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4cf9325de6845aa1235b153b42503a92c8604f447a6e6e402c891412a1157905
|
|
4
|
+
data.tar.gz: ce566e52e4889d22eb42dff316fa989b414d34f4d0b14d9595b509e5913c702f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c8da52aef46f781cdf227fc0494047bdbf64869ce7dea91797ca8a79f61d41fd3a490fdb332b10413b0b2be292c2f00e0b8c2703b944eeb57779a3a517327d94
|
|
7
|
+
data.tar.gz: c9c856269882482e5e9f838e5f1f7ca89ca2ea951296cc0b24f46b642b641277bab3f833e38f670f00ad1b878cb14a678986c15b80db6a3029c0e6e1033a309d
|
data/exe/rakit
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# CLI for rakit gem. Subcommands: file (list|copy), word-count (--json-keys), static-web-server (start|stop|running|publish).
|
|
5
|
+
|
|
6
|
+
require "rakit"
|
|
7
|
+
|
|
8
|
+
# Allow tests to override root via environment
|
|
9
|
+
Rakit::StaticWebServer.root = ENV["RAKIT_WWW_ROOT"] if ENV["RAKIT_WWW_ROOT"]
|
|
10
|
+
|
|
11
|
+
def usage_stderr(msg = nil)
|
|
12
|
+
$stderr.puts msg if msg
|
|
13
|
+
$stderr.puts "Usage: rakit <subcommand> [options] [args]"
|
|
14
|
+
$stderr.puts " file list <directory> [--recursive] [--include-hidden] [--format console|json|proto-json]"
|
|
15
|
+
$stderr.puts " file copy <source> <destination> [--overwrite] [--create-directories] [--format ...]"
|
|
16
|
+
$stderr.puts " word-count <file>|--stdin --json-keys [--format console|json|proto-json] [options]"
|
|
17
|
+
$stderr.puts " static-web-server <start|stop|running|publish> [options] [args]"
|
|
18
|
+
$stderr.puts " start [--port PORT] Start server (idempotent)"
|
|
19
|
+
$stderr.puts " stop Stop server"
|
|
20
|
+
$stderr.puts " running Exit 0 if running, non-zero otherwise"
|
|
21
|
+
$stderr.puts " publish <site_name> <source_dir> Publish static site"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def main(argv = ARGV)
|
|
25
|
+
return usage_stderr("Expected subcommand.") if argv.empty?
|
|
26
|
+
|
|
27
|
+
sub = argv[0]
|
|
28
|
+
if sub == "file"
|
|
29
|
+
argv.shift
|
|
30
|
+
require "rakit/cli/file"
|
|
31
|
+
return Rakit::CLI::File.run(argv)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if sub == "word-count" || sub == "word_count"
|
|
35
|
+
argv.shift
|
|
36
|
+
require "rakit/cli/word_count"
|
|
37
|
+
return Rakit::CLI::WordCount.run(argv)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if sub != "static-web-server"
|
|
41
|
+
usage_stderr("Unknown subcommand: #{sub}")
|
|
42
|
+
return 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
argv.shift
|
|
46
|
+
cmd = argv.shift
|
|
47
|
+
case cmd
|
|
48
|
+
when "start"
|
|
49
|
+
port = Rakit::StaticWebServer.port
|
|
50
|
+
while (arg = argv.shift)
|
|
51
|
+
if arg == "--port" && argv[0]
|
|
52
|
+
port = argv.shift.to_i
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
Rakit::StaticWebServer.start(port: port)
|
|
56
|
+
return 0
|
|
57
|
+
when "stop"
|
|
58
|
+
Rakit::StaticWebServer.stop
|
|
59
|
+
return 0
|
|
60
|
+
when "running"
|
|
61
|
+
return Rakit::StaticWebServer.running? ? 0 : 1
|
|
62
|
+
when "publish"
|
|
63
|
+
site_name = argv.shift
|
|
64
|
+
source_dir = argv.shift
|
|
65
|
+
unless site_name && source_dir
|
|
66
|
+
usage_stderr("publish requires <site_name> and <source_dir>")
|
|
67
|
+
return 1
|
|
68
|
+
end
|
|
69
|
+
Rakit::StaticWebServer.publish(site_name, source_dir)
|
|
70
|
+
return 0
|
|
71
|
+
else
|
|
72
|
+
usage_stderr(cmd ? "Unknown command: #{cmd}" : "Expected command after static-web-server.")
|
|
73
|
+
return 1
|
|
74
|
+
end
|
|
75
|
+
rescue ArgumentError, Errno::ENOENT, Errno::EACCES, Errno::EPERM => e
|
|
76
|
+
$stderr.puts e.message
|
|
77
|
+
return 1
|
|
78
|
+
rescue => e
|
|
79
|
+
$stderr.puts "Error: #{e.message}"
|
|
80
|
+
return 1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
exit main(ARGV.dup)
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
3
|
# source: azure.devops.proto
|
|
4
4
|
|
|
5
|
-
require
|
|
5
|
+
require 'google/protobuf'
|
|
6
|
+
|
|
6
7
|
|
|
7
8
|
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
|
|
data/lib/generated/data_pb.rb
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
3
|
# source: data.proto
|
|
4
4
|
|
|
5
|
-
require
|
|
6
|
-
|
|
5
|
+
require "google/protobuf"
|
|
7
6
|
|
|
8
7
|
descriptor_data = "\n\ndata.proto\x12\nrakit.data\"h\n\x05Index\x12/\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x1e.rakit.data.Index.EntriesEntry\x1a.\n\x0c\x45ntriesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01*^\n\x0c\x45xportFormat\x12\x19\n\x15PROTOBUF_BINARY_FILES\x10\x00\x12\x17\n\x13PROTOBUF_JSON_FILES\x10\x01\x12\x1a\n\x16PROTOBUF_BINARY_ZIPPED\x10\x02\x42\x0e\xea\x02\x0bRakit::Datab\x06proto3"
|
|
9
8
|
|
data/lib/generated/example_pb.rb
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
3
|
# source: example.proto
|
|
4
4
|
|
|
5
|
-
require
|
|
5
|
+
require 'google/protobuf'
|
|
6
|
+
|
|
6
7
|
|
|
7
8
|
descriptor_data = "\n\rexample.proto\x12\rrakit.example\"\x1c\n\x0cHelloRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\" \n\rHelloResponse\x12\x0f\n\x07message\x18\x01 \x01(\tB\x13\xea\x02\x10Rakit::Generatedb\x06proto3"
|
|
8
9
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: rakit.file.proto
|
|
4
|
+
|
|
5
|
+
require 'google/protobuf'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
descriptor_data = "\n\x10rakit.file.proto\x12\nrakit.file\"T\n\nFileConfig\x12\x1a\n\x12\x63reate_directories\x18\x01 \x01(\x08\x12\x11\n\toverwrite\x18\x02 \x01(\x08\x12\x17\n\x0f\x66ollow_symlinks\x18\x03 \x01(\x08\"\x81\x01\n\x0bListRequest\x12&\n\x06\x63onfig\x18\x01 \x01(\x0b\x32\x16.rakit.file.FileConfig\x12\x11\n\tdirectory\x18\x02 \x01(\t\x12\x11\n\trecursive\x18\x03 \x01(\x08\x12\x0c\n\x04glob\x18\x04 \x01(\t\x12\x16\n\x0einclude_hidden\x18\x05 \x01(\x08\"\x7f\n\tFileEntry\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x14\n\x0cis_directory\x18\x03 \x01(\x08\x12\x12\n\nis_symlink\x18\x04 \x01(\x08\x12\x12\n\nsize_bytes\x18\x05 \x01(\x03\x12\x18\n\x10modified_unix_ms\x18\x06 \x01(\x03\"y\n\nListResult\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12&\n\x07\x65ntries\x18\x03 \x03(\x0b\x32\x15.rakit.file.FileEntry\x12\x11\n\texit_code\x18\x04 \x01(\x05\x12\x0e\n\x06stderr\x18\x05 \x01(\t\"Z\n\x0b\x43opyRequest\x12&\n\x06\x63onfig\x18\x01 \x01(\x0b\x32\x16.rakit.file.FileConfig\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65stination\x18\x03 \x01(\t\"\x8c\x01\n\nCopyResult\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x0e\n\x06source\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x65stination\x18\x04 \x01(\t\x12\x11\n\texit_code\x18\x05 \x01(\x05\x12\x0e\n\x06stderr\x18\x06 \x01(\t\x12\x14\n\x0c\x62ytes_copied\x18\x07 \x01(\x03\x62\x06proto3"
|
|
9
|
+
|
|
10
|
+
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
11
|
+
pool.add_serialized_file(descriptor_data)
|
|
12
|
+
|
|
13
|
+
module Rakit
|
|
14
|
+
module File
|
|
15
|
+
FileConfig = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.file.FileConfig").msgclass
|
|
16
|
+
ListRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.file.ListRequest").msgclass
|
|
17
|
+
FileEntry = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.file.FileEntry").msgclass
|
|
18
|
+
ListResult = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.file.ListResult").msgclass
|
|
19
|
+
CopyRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.file.CopyRequest").msgclass
|
|
20
|
+
CopyResult = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.file.CopyResult").msgclass
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: rakit.word_count.proto
|
|
4
|
+
|
|
5
|
+
require 'google/protobuf'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
descriptor_data = "\n\x16rakit.word_count.proto\x12\x10rakit.word_count\"\x9c\x01\n\x0fWordCountConfig\x12\x18\n\x10\x63\x61se_insensitive\x18\x01 \x01(\x08\x12\x18\n\x10split_camel_case\x18\x02 \x01(\x08\x12\x19\n\x11split_snake_kebab\x18\x03 \x01(\x08\x12\x18\n\x10min_token_length\x18\x04 \x01(\r\x12\x11\n\tstopwords\x18\x05 \x03(\t\x12\r\n\x05top_n\x18\x06 \x01(\r\"S\n\nJsonSource\x12\x13\n\tjson_file\x18\x01 \x01(\tH\x00\x12\x15\n\x0binline_json\x18\x02 \x01(\tH\x00\x12\x0f\n\x05stdin\x18\x03 \x01(\x08H\x00\x42\x08\n\x06source\"\xbb\x01\n\x10WordCountRequest\x12\x31\n\x06\x63onfig\x18\x01 \x01(\x0b\x32!.rakit.word_count.WordCountConfig\x12*\n\x04json\x18\x02 \x01(\x0b\x32\x1c.rakit.word_count.JsonSource\x12\x31\n\x04mode\x18\x03 \x01(\x0e\x32#.rakit.word_count.JsonWordCountMode\x12\x15\n\rinclude_paths\x18\x04 \x01(\x08\"*\n\nTokenCount\x12\r\n\x05token\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\x04\"\xb1\x01\n\x0fWordCountResult\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12,\n\x06\x63ounts\x18\x03 \x03(\x0b\x32\x1c.rakit.word_count.TokenCount\x12\x11\n\texit_code\x18\x04 \x01(\x05\x12\x0e\n\x06stderr\x18\x05 \x01(\t\x12\x14\n\x0ctotal_tokens\x18\x06 \x01(\x04\x12\x15\n\runique_tokens\x18\x07 \x01(\x04*y\n\x11JsonWordCountMode\x12$\n JSON_WORD_COUNT_MODE_UNSPECIFIED\x10\x00\x12\x1d\n\x19JSON_WORD_COUNT_MODE_KEYS\x10\x01\x12\x1f\n\x1bJSON_WORD_COUNT_MODE_VALUES\x10\x02\x42\x13\xea\x02\x10Rakit::Generatedb\x06proto3"
|
|
9
|
+
|
|
10
|
+
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
11
|
+
pool.add_serialized_file(descriptor_data)
|
|
12
|
+
|
|
13
|
+
module Rakit
|
|
14
|
+
module Generated
|
|
15
|
+
WordCountConfig = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.word_count.WordCountConfig").msgclass
|
|
16
|
+
JsonSource = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.word_count.JsonSource").msgclass
|
|
17
|
+
WordCountRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.word_count.WordCountRequest").msgclass
|
|
18
|
+
TokenCount = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.word_count.TokenCount").msgclass
|
|
19
|
+
WordCountResult = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.word_count.WordCountResult").msgclass
|
|
20
|
+
JsonWordCountMode = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.word_count.JsonWordCountMode").enummodule
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: static_web_server.proto
|
|
4
|
+
|
|
5
|
+
require 'google/protobuf'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
descriptor_data = "\n\x17static_web_server.proto\x12\x17rakit.static_web_server\"T\n\x15StaticWebServerConfig\x12\x16\n\x0eroot_directory\x18\x01 \x01(\t\x12\x0c\n\x04port\x18\x02 \x01(\r\x12\x15\n\rhttps_enabled\x18\x03 \x01(\x08\"=\n\x0ePublishRequest\x12\x11\n\tsite_name\x18\x01 \x01(\t\x12\x18\n\x10source_directory\x18\x02 \x01(\t\"1\n\rPublishResult\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\"-\n\x0cServerStatus\x12\x0f\n\x07running\x18\x01 \x01(\x08\x12\x0c\n\x04port\x18\x02 \x01(\rB\x13\xea\x02\x10Rakit::Generatedb\x06proto3"
|
|
9
|
+
|
|
10
|
+
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
11
|
+
pool.add_serialized_file(descriptor_data)
|
|
12
|
+
|
|
13
|
+
module Rakit
|
|
14
|
+
module Generated
|
|
15
|
+
StaticWebServerConfig = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.static_web_server.StaticWebServerConfig").msgclass
|
|
16
|
+
PublishRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.static_web_server.PublishRequest").msgclass
|
|
17
|
+
PublishResult = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.static_web_server.PublishResult").msgclass
|
|
18
|
+
ServerStatus = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.static_web_server.ServerStatus").msgclass
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "google/protobuf"
|
|
5
|
+
require "rakit/word_count"
|
|
6
|
+
|
|
7
|
+
module Rakit
|
|
8
|
+
module CLI
|
|
9
|
+
module WordCount
|
|
10
|
+
class << self
|
|
11
|
+
# @param argv [Array<String>] arguments after "word-count" / "word_count"
|
|
12
|
+
# @return [Integer] exit code
|
|
13
|
+
def run(argv)
|
|
14
|
+
opts = parse_argv(argv)
|
|
15
|
+
return 1 if opts[:error]
|
|
16
|
+
|
|
17
|
+
request = build_request(opts)
|
|
18
|
+
return 1 unless request
|
|
19
|
+
|
|
20
|
+
result = Rakit::WordCount.count(request)
|
|
21
|
+
render_result(result, opts[:format])
|
|
22
|
+
result.exit_code || (result.success ? 0 : 1)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def parse_argv(argv)
|
|
28
|
+
opts = {
|
|
29
|
+
format: "console",
|
|
30
|
+
case_insensitive: false,
|
|
31
|
+
split_camel_case: false,
|
|
32
|
+
split_snake_kebab: false,
|
|
33
|
+
min_token_length: 0,
|
|
34
|
+
top_n: 0,
|
|
35
|
+
stopwords: [],
|
|
36
|
+
json_keys: false,
|
|
37
|
+
stdin: false,
|
|
38
|
+
json_file: nil
|
|
39
|
+
}
|
|
40
|
+
args = argv.dup
|
|
41
|
+
while (arg = args.shift)
|
|
42
|
+
case arg
|
|
43
|
+
when "--json-keys" then opts[:json_keys] = true
|
|
44
|
+
when "--stdin" then opts[:stdin] = true
|
|
45
|
+
when "--format"
|
|
46
|
+
opts[:format] = args.shift || "console"
|
|
47
|
+
when "--case-insensitive" then opts[:case_insensitive] = true
|
|
48
|
+
when "--split-camel-case" then opts[:split_camel_case] = true
|
|
49
|
+
when "--split-snake-kebab" then opts[:split_snake_kebab] = true
|
|
50
|
+
when "--min-token-length"
|
|
51
|
+
opts[:min_token_length] = (args.shift || "0").to_i
|
|
52
|
+
when "--top"
|
|
53
|
+
opts[:top_n] = (args.shift || "0").to_i
|
|
54
|
+
when "--stopword"
|
|
55
|
+
opts[:stopwords] << args.shift if args[0]
|
|
56
|
+
when /^--/
|
|
57
|
+
$stderr.puts "Unknown option: #{arg}"
|
|
58
|
+
opts[:error] = true
|
|
59
|
+
else
|
|
60
|
+
opts[:json_file] = arg
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
opts
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def build_request(opts)
|
|
67
|
+
unless opts[:json_keys]
|
|
68
|
+
$stderr.puts "word-count requires --json-keys for MVP"
|
|
69
|
+
return nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
gen = Rakit::Generated
|
|
73
|
+
config = gen::WordCountConfig.new(
|
|
74
|
+
case_insensitive: opts[:case_insensitive],
|
|
75
|
+
split_camel_case: opts[:split_camel_case],
|
|
76
|
+
split_snake_kebab: opts[:split_snake_kebab],
|
|
77
|
+
min_token_length: opts[:min_token_length],
|
|
78
|
+
stopwords: opts[:stopwords],
|
|
79
|
+
top_n: opts[:top_n]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if opts[:stdin]
|
|
83
|
+
json_str = $stdin.read
|
|
84
|
+
json = gen::JsonSource.new(inline_json: json_str)
|
|
85
|
+
elsif opts[:json_file] && !opts[:json_file].empty?
|
|
86
|
+
json = gen::JsonSource.new(json_file: opts[:json_file])
|
|
87
|
+
else
|
|
88
|
+
$stderr.puts "Provide a JSON file path or use --stdin"
|
|
89
|
+
return nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
gen::WordCountRequest.new(
|
|
93
|
+
config: config,
|
|
94
|
+
json: json,
|
|
95
|
+
mode: :JSON_WORD_COUNT_MODE_KEYS
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def render_result(result, format)
|
|
100
|
+
case format
|
|
101
|
+
when "json"
|
|
102
|
+
puts result_to_json(result)
|
|
103
|
+
when "proto-json"
|
|
104
|
+
puts Google::Protobuf.encode_json(result)
|
|
105
|
+
else
|
|
106
|
+
render_console(result)
|
|
107
|
+
end
|
|
108
|
+
$stderr.puts result.stderr if result.stderr && !result.stderr.empty? && !result.success
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def render_console(result)
|
|
112
|
+
result.counts.each do |tc|
|
|
113
|
+
puts "#{tc.token}\t#{tc.count}"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def result_to_json(result)
|
|
118
|
+
h = {
|
|
119
|
+
success: result.success,
|
|
120
|
+
message: result.message.to_s,
|
|
121
|
+
counts: result.counts.map { |tc| { token: tc.token, count: tc.count } },
|
|
122
|
+
exit_code: result.exit_code,
|
|
123
|
+
total_tokens: result.total_tokens,
|
|
124
|
+
unique_tokens: result.unique_tokens
|
|
125
|
+
}
|
|
126
|
+
JSON.generate(h)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
data/lib/rakit/gem.rb
CHANGED
|
@@ -11,29 +11,29 @@ module Rakit
|
|
|
11
11
|
FileUtils.mkdir_p(out_dir)
|
|
12
12
|
gem_file = ::Gem::Package.build(spec)
|
|
13
13
|
FileUtils.mv(gem_file, out_dir)
|
|
14
|
-
File.join(out_dir, gem_file)
|
|
14
|
+
::File.join(out_dir, gem_file)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
# Publish the gem to rubygems.org. Loads the gemspec from gemspec_path and
|
|
18
18
|
# expects the .gem file in dirname(gemspec_path)/artifacts/. Run package first.
|
|
19
19
|
def self.publish(gemspec_path)
|
|
20
|
-
path = File.expand_path(gemspec_path)
|
|
20
|
+
path = ::File.expand_path(gemspec_path)
|
|
21
21
|
spec = ::Gem::Specification.load(path)
|
|
22
|
-
out_dir = File.join(File.dirname(path), "artifacts")
|
|
23
|
-
gem_path = File.join(out_dir, "#{spec.full_name}.gem")
|
|
22
|
+
out_dir = ::File.join(::File.dirname(path), "artifacts")
|
|
23
|
+
gem_path = ::File.join(out_dir, "#{spec.full_name}.gem")
|
|
24
24
|
push(gem_path)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# Bump the last digit of the version in the gemspec file (e.g. "0.1.0" -> "0.1.1").
|
|
28
28
|
# Writes the file in place. Returns the new version string.
|
|
29
29
|
def self.bump(gemspec_path)
|
|
30
|
-
content = File.read(gemspec_path)
|
|
30
|
+
content = ::File.read(gemspec_path)
|
|
31
31
|
content.sub!(/^(\s*s\.version\s*=\s*["'])([\d.]+)(["'])/) do
|
|
32
32
|
segs = Regexp.last_match(2).split(".")
|
|
33
33
|
segs[-1] = (segs[-1].to_i + 1).to_s
|
|
34
34
|
"#{Regexp.last_match(1)}#{segs.join(".")}#{Regexp.last_match(3)}"
|
|
35
35
|
end or raise "No s.version line found in #{gemspec_path}"
|
|
36
|
-
File.write(gemspec_path, content)
|
|
36
|
+
::File.write(gemspec_path, content)
|
|
37
37
|
content[/s\.version\s*=\s*["']([^"']+)["']/, 1]
|
|
38
38
|
end
|
|
39
39
|
|
|
@@ -41,9 +41,9 @@ module Rakit
|
|
|
41
41
|
# published, warns and returns without pushing. Raises if the file is missing
|
|
42
42
|
# or if gem push fails.
|
|
43
43
|
def self.push(gem_path)
|
|
44
|
-
raise "Gem not found: #{gem_path}. Run rake package first." unless File.file?(gem_path)
|
|
44
|
+
raise "Gem not found: #{gem_path}. Run rake package first." unless ::File.file?(gem_path)
|
|
45
45
|
|
|
46
|
-
base = File.basename(gem_path, ".gem")
|
|
46
|
+
base = ::File.basename(gem_path, ".gem")
|
|
47
47
|
parts = base.split("-")
|
|
48
48
|
version = parts.pop
|
|
49
49
|
name = parts.join("-")
|
data/lib/rakit/git.rb
CHANGED
|
@@ -4,12 +4,18 @@ module Rakit
|
|
|
4
4
|
module Git
|
|
5
5
|
# Sync the current directory with the remote (git pull, then git push).
|
|
6
6
|
# Runs from Dir.pwd. Raises if not a git repo or if pull/push fails.
|
|
7
|
+
# If the current branch has no remote tracking branch, pull and push are skipped (no error).
|
|
7
8
|
def self.sync(dir = nil)
|
|
8
9
|
require_relative "shell"
|
|
9
|
-
target = dir ? File.expand_path(dir) : Dir.pwd
|
|
10
|
-
raise "Not a git repository: #{target}" unless File.directory?(File.join(target, ".git"))
|
|
10
|
+
target = dir ? ::File.expand_path(dir) : Dir.pwd
|
|
11
|
+
raise "Not a git repository: #{target}" unless ::File.directory?(::File.join(target, ".git"))
|
|
11
12
|
|
|
12
13
|
Dir.chdir(target) do
|
|
14
|
+
check = Rakit::Shell.run("git rev-parse --abbrev-ref @{u}")
|
|
15
|
+
if check.exit_status != 0
|
|
16
|
+
# No upstream configured for current branch; skip sync
|
|
17
|
+
return
|
|
18
|
+
end
|
|
13
19
|
result = Rakit::Shell.run("git pull")
|
|
14
20
|
raise "git pull failed" unless result.exit_status == 0
|
|
15
21
|
result = Rakit::Shell.run("git push")
|
|
@@ -22,8 +28,8 @@ module Rakit
|
|
|
22
28
|
# Raises if not a git repo or if add fails.
|
|
23
29
|
def self.integrate(commit_message = nil, dir = nil)
|
|
24
30
|
require_relative "shell"
|
|
25
|
-
target = dir ? File.expand_path(dir) : Dir.pwd
|
|
26
|
-
raise "Not a git repository: #{target}" unless File.directory?(File.join(target, ".git"))
|
|
31
|
+
target = dir ? ::File.expand_path(dir) : Dir.pwd
|
|
32
|
+
raise "Not a git repository: #{target}" unless ::File.directory?(::File.join(target, ".git"))
|
|
27
33
|
|
|
28
34
|
message = commit_message || "Integrate"
|
|
29
35
|
Dir.chdir(target) do
|
data/lib/rakit/protobuf.rb
CHANGED
|
@@ -5,19 +5,21 @@ require "fileutils"
|
|
|
5
5
|
module Rakit
|
|
6
6
|
module Protobuf
|
|
7
7
|
# Generate Ruby from .proto files.
|
|
8
|
-
# proto_dir: directory containing .proto files (and -I root)
|
|
9
|
-
# ruby_out: directory for generated *_pb.rb files
|
|
8
|
+
# proto_dir: directory containing .proto files (and -I root); may be relative to base_dir
|
|
9
|
+
# ruby_out: directory for generated *_pb.rb files; may be relative to base_dir
|
|
10
|
+
# base_dir: directory used to expand relative proto_dir and ruby_out (default: Dir.pwd)
|
|
10
11
|
# protoc: executable name/path (default: "protoc")
|
|
11
12
|
# Returns ruby_out if generation ran, nil if no proto files found.
|
|
12
|
-
def self.generate(proto_dir:, ruby_out:, protoc: "protoc")
|
|
13
|
+
def self.generate(proto_dir:, ruby_out:, base_dir: Dir.pwd, protoc: "protoc")
|
|
14
|
+
expanded_proto_dir = File.expand_path(proto_dir, base_dir)
|
|
15
|
+
expanded_ruby_out = File.expand_path(ruby_out, base_dir)
|
|
13
16
|
# output, in grey, " generating code from .proto files..."
|
|
14
17
|
puts "\e[30m generating code from .proto files...\e[0m"
|
|
15
|
-
expanded_proto_dir = File.expand_path(proto_dir)
|
|
16
18
|
proto_files = Dir[File.join(expanded_proto_dir, "**", "*.proto")]
|
|
17
19
|
return nil if proto_files.empty?
|
|
18
20
|
|
|
19
|
-
FileUtils.mkdir_p(
|
|
20
|
-
args = ["-I", expanded_proto_dir, "--ruby_out=#{
|
|
21
|
+
FileUtils.mkdir_p(expanded_ruby_out)
|
|
22
|
+
args = ["-I", expanded_proto_dir, "--ruby_out=#{expanded_ruby_out}", *proto_files]
|
|
21
23
|
system(protoc, *args) or raise "protoc failed"
|
|
22
24
|
|
|
23
25
|
# output a green checkmark and the command that was run
|
|
@@ -25,14 +27,14 @@ module Rakit
|
|
|
25
27
|
# output that the files were generated
|
|
26
28
|
#puts " Generated #{proto_files.size} files in #{ruby_out}"
|
|
27
29
|
# output the files that were generated (all files in the ruby_out directory), once per line
|
|
28
|
-
ruby_out_files = Dir[File.join(
|
|
30
|
+
ruby_out_files = Dir[File.join(expanded_ruby_out, "**", "*_pb.rb")]
|
|
29
31
|
ruby_out_files.each do |file|
|
|
30
32
|
# output, in grey, " #{File.basename(file)}"
|
|
31
33
|
puts "\e[30m #{File.basename(file)}\e[0m"
|
|
32
34
|
end
|
|
33
35
|
# output the number of files that were generated
|
|
34
|
-
#puts " Generated #{ruby_out_files.size} files in #{
|
|
35
|
-
|
|
36
|
+
#puts " Generated #{ruby_out_files.size} files in #{expanded_ruby_out}"
|
|
37
|
+
expanded_ruby_out
|
|
36
38
|
end
|
|
37
39
|
end
|
|
38
40
|
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "generated/static_web_server_pb"
|
|
5
|
+
|
|
6
|
+
module Rakit
|
|
7
|
+
# Publish static sites into a configurable root, regenerate a root index, and control a local HTTP server (start/stop/running).
|
|
8
|
+
# CLI: +rakit static-web-server+ (start|stop|running|publish). See contracts/ruby-api.md and quickstart in specs/003-static-web-server/quickstart.md.
|
|
9
|
+
module StaticWebServer
|
|
10
|
+
SITE_NAME_REGEX = /\A[a-z0-9\-]+\z/.freeze
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# @return [String] Static root directory (default ~/.rakit/www_root). Used by publish and server lifecycle.
|
|
14
|
+
attr_accessor :root
|
|
15
|
+
# @return [Integer] Default port for start (default 5099). Used when no override passed to start.
|
|
16
|
+
attr_accessor :port
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
self.root = ::File.expand_path("~/.rakit/www_root")
|
|
20
|
+
self.port = 5099
|
|
21
|
+
|
|
22
|
+
# T005: Validate site name; raise ArgumentError before any filesystem write.
|
|
23
|
+
def self.validate_site_name!(site_name)
|
|
24
|
+
return if site_name.is_a?(String) && site_name.match?(SITE_NAME_REGEX)
|
|
25
|
+
raise ArgumentError, "site_name must be lowercase alphanumeric and dashes only (e.g. my-site); got: #{site_name.inspect}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# T006: PID file path for start/stop/running (research: ~/.rakit/static_web_server.pid).
|
|
29
|
+
def self.pid_file_path
|
|
30
|
+
::File.expand_path("~/.rakit/static_web_server.pid")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# T007: Server binary (miniserve) on PATH; used by start. Returns path or nil.
|
|
34
|
+
def self.server_binary
|
|
35
|
+
@server_binary ||= begin
|
|
36
|
+
path = ENV["RAKIT_STATIC_SERVER_BINARY"]
|
|
37
|
+
return path if path && !path.empty?
|
|
38
|
+
which("miniserve")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.server_binary=(path)
|
|
43
|
+
@server_binary = path
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# T008: Create root directory if missing; used before publish and start.
|
|
47
|
+
def self.ensure_root
|
|
48
|
+
FileUtils.mkdir_p(root)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Publish static content from source_directory to root/site_name (atomic copy), then regenerate root index.
|
|
52
|
+
# @param site_name [String] Must match \\A[a-z0-9\-]+\\z (lowercase, alphanumeric, dashes only).
|
|
53
|
+
# @param source_directory [String] Existing directory path; contents are copied (no traversal outside allowed paths).
|
|
54
|
+
# @return [true] on success.
|
|
55
|
+
# @raise [ArgumentError] for invalid site_name, missing/invalid source_directory, or root not writable.
|
|
56
|
+
def self.publish(site_name, source_directory)
|
|
57
|
+
validate_site_name!(site_name)
|
|
58
|
+
src = ::File.expand_path(source_directory)
|
|
59
|
+
raise ArgumentError, "source_directory does not exist: #{source_directory}" unless ::File.exist?(src)
|
|
60
|
+
raise ArgumentError, "source_directory is not a directory: #{source_directory}" unless ::File.directory?(src)
|
|
61
|
+
|
|
62
|
+
ensure_root
|
|
63
|
+
# Check root is writable before we do any copy (T015 / edge case).
|
|
64
|
+
unless ::File.writable?(root)
|
|
65
|
+
raise ArgumentError, "root directory is not writable: #{root}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
target = ::File.join(root, site_name)
|
|
69
|
+
temp = ::File.join(root, ".tmp_#{site_name}_#{Process.pid}_#{rand(1_000_000)}")
|
|
70
|
+
FileUtils.mkdir_p(temp)
|
|
71
|
+
FileUtils.cp_r(::File.join(src, "."), temp)
|
|
72
|
+
FileUtils.rm_rf(target)
|
|
73
|
+
FileUtils.mv(temp, target)
|
|
74
|
+
regenerate_root_index
|
|
75
|
+
true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Whether the server process is currently running (PID file + process check). No side effects.
|
|
79
|
+
# @return [Boolean] true if server is running, false otherwise.
|
|
80
|
+
def self.running?
|
|
81
|
+
path = pid_file_path
|
|
82
|
+
return false unless ::File.file?(path)
|
|
83
|
+
pid = ::File.read(path).strip.to_i
|
|
84
|
+
return false if pid <= 0
|
|
85
|
+
Process.getpgid(pid)
|
|
86
|
+
true
|
|
87
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
88
|
+
false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Start background server serving root. Idempotent: if already running, no-op.
|
|
92
|
+
# @param options [Hash] :port (optional) override default port.
|
|
93
|
+
# @return [true] if server is running (started or already running).
|
|
94
|
+
# @raise [RuntimeError] if port in use or server binary (miniserve) not found.
|
|
95
|
+
def self.start(options = {})
|
|
96
|
+
return true if running?
|
|
97
|
+
ensure_root
|
|
98
|
+
p = options[:port] || port
|
|
99
|
+
raise "port #{p} is already in use; change port or stop the other process" if port_in_use?(p)
|
|
100
|
+
bin = server_binary
|
|
101
|
+
raise "static server binary (miniserve) not found on PATH; install miniserve or set RAKIT_STATIC_SERVER_BINARY" unless bin
|
|
102
|
+
root_path = ::File.expand_path(root)
|
|
103
|
+
pid = spawn(bin, root_path, "--port", p.to_s, out: ::File::NULL, err: ::File::NULL)
|
|
104
|
+
Process.detach(pid)
|
|
105
|
+
::File.write(pid_file_path, pid.to_s)
|
|
106
|
+
true
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
STOP_TIMEOUT = 5
|
|
110
|
+
|
|
111
|
+
# Gracefully terminate server process; remove PID file. Fail-fast if not running.
|
|
112
|
+
# @return [true] if stopped.
|
|
113
|
+
# @raise [RuntimeError] if server was not running (no PID file or process not found).
|
|
114
|
+
def self.stop
|
|
115
|
+
path = pid_file_path
|
|
116
|
+
raise "server is not running (no PID file at #{path})" unless ::File.file?(path)
|
|
117
|
+
pid = ::File.read(path).strip.to_i
|
|
118
|
+
raise "server is not running (invalid PID in #{path})" if pid <= 0
|
|
119
|
+
begin
|
|
120
|
+
Process.getpgid(pid)
|
|
121
|
+
rescue Errno::ESRCH
|
|
122
|
+
::File.delete(path)
|
|
123
|
+
raise "server is not running (process #{pid} not found)"
|
|
124
|
+
end
|
|
125
|
+
Process.kill(:TERM, pid)
|
|
126
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + STOP_TIMEOUT
|
|
127
|
+
while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
|
|
128
|
+
begin
|
|
129
|
+
Process.wait(pid, Process::WNOHANG)
|
|
130
|
+
break
|
|
131
|
+
rescue Errno::ECHILD
|
|
132
|
+
break
|
|
133
|
+
end
|
|
134
|
+
sleep 0.1
|
|
135
|
+
end
|
|
136
|
+
begin
|
|
137
|
+
Process.getpgid(pid)
|
|
138
|
+
Process.kill(:KILL, pid)
|
|
139
|
+
Process.wait(pid)
|
|
140
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
141
|
+
# already gone
|
|
142
|
+
end
|
|
143
|
+
::File.delete(path) if ::File.file?(path)
|
|
144
|
+
true
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# T017: Write root/index.html listing all site subdirectories alphabetically with links.
|
|
148
|
+
def self.regenerate_root_index
|
|
149
|
+
entries = Dir.entries(root).sort.select do |e|
|
|
150
|
+
e != "." && e != ".." && e != "index.html" && !e.start_with?(".") && ::File.directory?(::File.join(root, e))
|
|
151
|
+
end
|
|
152
|
+
html = ["<!DOCTYPE html>", "<html><head><meta charset=\"utf-8\"><title>Published sites</title></head><body>", "<h1>Published sites</h1>", "<ul>"]
|
|
153
|
+
entries.each { |name| html << "<li><a href=\"/#{name}/\">#{name}</a></li>" }
|
|
154
|
+
html << "</ul></body></html>"
|
|
155
|
+
::File.write(::File.join(root, "index.html"), html.join("\n"))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
class << self
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
def which(cmd)
|
|
162
|
+
exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
|
|
163
|
+
ENV["PATH"].to_s.split(::File::PATH_SEPARATOR).each do |dir|
|
|
164
|
+
next if dir.empty?
|
|
165
|
+
exts.each do |ext|
|
|
166
|
+
exe = ::File.join(dir, "#{cmd}#{ext}")
|
|
167
|
+
return exe if ::File.executable?(exe) && !::File.directory?(exe)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def port_in_use?(p)
|
|
174
|
+
require "socket"
|
|
175
|
+
TCPServer.new("127.0.0.1", p).close
|
|
176
|
+
false
|
|
177
|
+
rescue Errno::EADDRINUSE
|
|
178
|
+
true
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
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,11 @@ 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
|
|
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
|
-
|
|
27
|
+
require_relative "rakit/static_web_server"
|
|
28
|
+
require_relative "rakit/word_count"
|
|
29
|
+
require_relative "rakit/file"
|
|
28
30
|
require_relative "rakit/azure/dev_ops"
|
|
29
31
|
|
|
30
32
|
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
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rakit
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
@@ -93,22 +93,29 @@ dependencies:
|
|
|
93
93
|
- - "~>"
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: '5.0'
|
|
96
|
-
executables:
|
|
96
|
+
executables:
|
|
97
|
+
- rakit
|
|
97
98
|
extensions: []
|
|
98
99
|
extra_rdoc_files: []
|
|
99
100
|
files:
|
|
101
|
+
- exe/rakit
|
|
100
102
|
- lib/generated/azure.devops_pb.rb
|
|
101
103
|
- lib/generated/data_pb.rb
|
|
102
104
|
- lib/generated/example_pb.rb
|
|
105
|
+
- lib/generated/rakit.file_pb.rb
|
|
106
|
+
- lib/generated/rakit.word_count_pb.rb
|
|
103
107
|
- lib/generated/shell_pb.rb
|
|
108
|
+
- lib/generated/static_web_server_pb.rb
|
|
104
109
|
- lib/rakit.rb
|
|
105
110
|
- lib/rakit/azure/dev_ops.rb
|
|
106
|
-
- lib/rakit/
|
|
111
|
+
- lib/rakit/cli/word_count.rb
|
|
107
112
|
- lib/rakit/gem.rb
|
|
108
113
|
- lib/rakit/git.rb
|
|
109
114
|
- lib/rakit/protobuf.rb
|
|
110
115
|
- lib/rakit/shell.rb
|
|
116
|
+
- lib/rakit/static_web_server.rb
|
|
111
117
|
- lib/rakit/task.rb
|
|
118
|
+
- lib/rakit/word_count.rb
|
|
112
119
|
homepage: https://gitlab.com/gems/rakit
|
|
113
120
|
licenses:
|
|
114
121
|
- MIT
|
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
|