ukiryu 0.1.1 → 0.1.3
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/.github/workflows/release.yml +58 -14
- data/.gitignore +3 -0
- data/.rubocop_todo.yml +170 -79
- data/Gemfile +1 -1
- data/README.adoc +1603 -576
- data/docs/.gitignore +1 -0
- data/docs/Gemfile +10 -0
- data/docs/INDEX.adoc +261 -0
- data/docs/_config.yml +180 -0
- data/docs/advanced/custom-tool-classes.adoc +581 -0
- data/docs/advanced/index.adoc +20 -0
- data/docs/features/configuration.adoc +657 -0
- data/docs/features/index.adoc +31 -0
- data/docs/features/platform-support.adoc +488 -0
- data/docs/getting-started/core-concepts.adoc +666 -0
- data/docs/getting-started/index.adoc +36 -0
- data/docs/getting-started/installation.adoc +216 -0
- data/docs/getting-started/quick-start.adoc +258 -0
- data/docs/guides/env-var-sets.adoc +388 -0
- data/docs/guides/index.adoc +20 -0
- data/docs/interfaces/cli.adoc +609 -0
- data/docs/interfaces/index.adoc +153 -0
- data/docs/interfaces/ruby-api.adoc +538 -0
- data/docs/lychee.toml +49 -0
- data/docs/reference/configuration-options.adoc +720 -0
- data/docs/reference/error-codes.adoc +634 -0
- data/docs/reference/index.adoc +20 -0
- data/docs/reference/ruby-api.adoc +1217 -0
- data/docs/understanding/index.adoc +20 -0
- data/lib/ukiryu/cli.rb +43 -58
- data/lib/ukiryu/cli_commands/base_command.rb +16 -27
- data/lib/ukiryu/cli_commands/cache_command.rb +100 -0
- data/lib/ukiryu/cli_commands/commands_command.rb +8 -8
- data/lib/ukiryu/cli_commands/commands_command.rb.fixed +1 -1
- data/lib/ukiryu/cli_commands/config_command.rb +49 -7
- data/lib/ukiryu/cli_commands/definitions_command.rb +254 -0
- data/lib/ukiryu/cli_commands/describe_command.rb +13 -7
- data/lib/ukiryu/cli_commands/describe_command.rb.fixed +1 -1
- data/lib/ukiryu/cli_commands/docs_command.rb +148 -0
- data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +1 -1
- data/lib/ukiryu/cli_commands/extract_command.rb +2 -2
- data/lib/ukiryu/cli_commands/info_command.rb +7 -7
- data/lib/ukiryu/cli_commands/lint_command.rb +167 -0
- data/lib/ukiryu/cli_commands/list_command.rb +6 -6
- data/lib/ukiryu/cli_commands/opts_command.rb +2 -2
- data/lib/ukiryu/cli_commands/opts_command.rb.fixed +1 -1
- data/lib/ukiryu/cli_commands/register_command.rb +144 -0
- data/lib/ukiryu/cli_commands/resolve_command.rb +124 -0
- data/lib/ukiryu/cli_commands/run_command.rb +38 -14
- data/lib/ukiryu/cli_commands/run_file_command.rb +2 -2
- data/lib/ukiryu/cli_commands/system_command.rb +50 -32
- data/lib/ukiryu/cli_commands/validate_command.rb +452 -51
- data/lib/ukiryu/cli_commands/which_command.rb +5 -5
- data/lib/ukiryu/command_builder.rb +81 -23
- data/lib/ukiryu/config/env_provider.rb +3 -3
- data/lib/ukiryu/config/env_schema.rb +6 -6
- data/lib/ukiryu/config.rb +11 -11
- data/lib/ukiryu/definition/definition_cache.rb +238 -0
- data/lib/ukiryu/definition/definition_composer.rb +257 -0
- data/lib/ukiryu/definition/definition_linter.rb +460 -0
- data/lib/ukiryu/definition/definition_validator.rb +320 -0
- data/lib/ukiryu/definition/discovery.rb +239 -0
- data/lib/ukiryu/definition/documentation_generator.rb +429 -0
- data/lib/ukiryu/definition/lint_issue.rb +168 -0
- data/lib/ukiryu/definition/loader.rb +139 -0
- data/lib/ukiryu/definition/metadata.rb +159 -0
- data/lib/ukiryu/definition/source.rb +87 -0
- data/lib/ukiryu/definition/sources/file.rb +138 -0
- data/lib/ukiryu/definition/sources/string.rb +88 -0
- data/lib/ukiryu/definition/validation_result.rb +158 -0
- data/lib/ukiryu/definition/version_resolver.rb +194 -0
- data/lib/ukiryu/definition.rb +40 -0
- data/lib/ukiryu/errors.rb +6 -0
- data/lib/ukiryu/execution_context.rb +11 -11
- data/lib/ukiryu/executor.rb +6 -0
- data/lib/ukiryu/extractors/extractor.rb +6 -5
- data/lib/ukiryu/extractors/help_parser.rb +13 -19
- data/lib/ukiryu/logger.rb +3 -1
- data/lib/ukiryu/models/command_definition.rb +3 -3
- data/lib/ukiryu/models/command_info.rb +1 -1
- data/lib/ukiryu/models/components.rb +1 -3
- data/lib/ukiryu/models/env_var_definition.rb +11 -3
- data/lib/ukiryu/models/flag_definition.rb +15 -0
- data/lib/ukiryu/models/option_definition.rb +7 -7
- data/lib/ukiryu/models/platform_profile.rb +6 -3
- data/lib/ukiryu/models/routing.rb +1 -1
- data/lib/ukiryu/models/tool_definition.rb +2 -4
- data/lib/ukiryu/models/tool_metadata.rb +6 -6
- data/lib/ukiryu/models/validation_result.rb +1 -1
- data/lib/ukiryu/models/version_compatibility.rb +6 -3
- data/lib/ukiryu/models/version_detection.rb +10 -1
- data/lib/ukiryu/{registry.rb → register.rb} +54 -38
- data/lib/ukiryu/register_auto_manager.rb +268 -0
- data/lib/ukiryu/schema_validator.rb +31 -10
- data/lib/ukiryu/shell/base.rb +18 -0
- data/lib/ukiryu/shell/bash.rb +19 -1
- data/lib/ukiryu/shell/cmd.rb +11 -1
- data/lib/ukiryu/shell/powershell.rb +11 -1
- data/lib/ukiryu/shell.rb +1 -1
- data/lib/ukiryu/tool.rb +107 -95
- data/lib/ukiryu/tool_index.rb +22 -22
- data/lib/ukiryu/tools/base.rb +12 -25
- data/lib/ukiryu/tools/generator.rb +7 -7
- data/lib/ukiryu/tools.rb +3 -3
- data/lib/ukiryu/type.rb +20 -5
- data/lib/ukiryu/version.rb +1 -1
- data/lib/ukiryu/version_detector.rb +21 -2
- data/lib/ukiryu.rb +6 -3
- data/ukiryu-proposal.md +41 -41
- data/ukiryu.gemspec +1 -0
- metadata +64 -8
- data/.gitmodules +0 -3
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'type'
|
|
4
4
|
require_relative 'shell'
|
|
5
|
+
require_relative 'models/env_var_definition'
|
|
5
6
|
|
|
6
7
|
module Ukiryu
|
|
7
8
|
# CommandBuilder module provides shared command building functionality.
|
|
@@ -23,7 +24,19 @@ module Ukiryu
|
|
|
23
24
|
# Add subcommand prefix if present (e.g., for ImageMagick "magick convert")
|
|
24
25
|
args << command.subcommand if command.subcommand
|
|
25
26
|
|
|
26
|
-
# Add
|
|
27
|
+
# Add prefix flags FIRST (must come before any options)
|
|
28
|
+
command.flags&.each do |flag_def|
|
|
29
|
+
param_key = flag_def.name_sym
|
|
30
|
+
next unless flag_def.position_constraint_sym == :prefix
|
|
31
|
+
|
|
32
|
+
value = params[param_key]
|
|
33
|
+
value = flag_def.default if value.nil?
|
|
34
|
+
|
|
35
|
+
formatted_flag = format_flag(flag_def, value)
|
|
36
|
+
Array(formatted_flag).each { |flag| args << flag unless flag.nil? || flag.empty? }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Add options (after prefix flags)
|
|
27
40
|
command.options&.each do |opt_def|
|
|
28
41
|
param_key = opt_def.name_sym
|
|
29
42
|
next unless params.key?(param_key)
|
|
@@ -33,9 +46,11 @@ module Ukiryu
|
|
|
33
46
|
Array(formatted_opt).each { |opt| args << opt unless opt.nil? || opt.empty? }
|
|
34
47
|
end
|
|
35
48
|
|
|
36
|
-
# Add flags
|
|
49
|
+
# Add non-prefix flags (after options)
|
|
37
50
|
command.flags&.each do |flag_def|
|
|
38
51
|
param_key = flag_def.name_sym
|
|
52
|
+
next if flag_def.position_constraint_sym == :prefix
|
|
53
|
+
|
|
39
54
|
value = params[param_key]
|
|
40
55
|
value = flag_def.default if value.nil?
|
|
41
56
|
|
|
@@ -127,43 +142,59 @@ module Ukiryu
|
|
|
127
142
|
end
|
|
128
143
|
|
|
129
144
|
cli = opt_def.cli || ''
|
|
130
|
-
|
|
145
|
+
delimiter_sym = opt_def.assignment_delimiter_sym
|
|
131
146
|
separator = opt_def.separator || '='
|
|
132
147
|
|
|
148
|
+
# Auto-detect delimiter based on CLI prefix
|
|
149
|
+
delimiter_sym = detect_delimiter(cli) if delimiter_sym == :auto
|
|
150
|
+
|
|
133
151
|
# Convert value to string (handle symbols)
|
|
134
152
|
value_str = value.is_a?(Symbol) ? value.to_s : value.to_s
|
|
135
153
|
|
|
136
154
|
# Handle array values with separator
|
|
137
155
|
if value.is_a?(Array) && separator
|
|
138
156
|
joined = value.join(separator)
|
|
139
|
-
case
|
|
140
|
-
when :
|
|
141
|
-
"#{cli}
|
|
142
|
-
when :
|
|
157
|
+
case delimiter_sym
|
|
158
|
+
when :equals
|
|
159
|
+
"#{cli}=#{joined}"
|
|
160
|
+
when :space
|
|
143
161
|
[cli, joined] # Return array for space-separated
|
|
144
|
-
when :
|
|
145
|
-
"#{cli}
|
|
162
|
+
when :colon
|
|
163
|
+
"#{cli}:#{joined}"
|
|
164
|
+
when :none
|
|
165
|
+
cli
|
|
146
166
|
else
|
|
147
|
-
"#{cli}
|
|
167
|
+
"#{cli}=#{joined}"
|
|
148
168
|
end
|
|
149
169
|
else
|
|
150
|
-
case
|
|
151
|
-
when :
|
|
152
|
-
"#{cli}
|
|
153
|
-
when :
|
|
170
|
+
case delimiter_sym
|
|
171
|
+
when :equals
|
|
172
|
+
"#{cli}=#{value_str}"
|
|
173
|
+
when :space
|
|
154
174
|
[cli, value_str] # Return array for space-separated
|
|
155
|
-
when :
|
|
156
|
-
"#{cli}#{separator}#{value_str}"
|
|
157
|
-
when :slash_colon
|
|
175
|
+
when :colon
|
|
158
176
|
"#{cli}:#{value_str}"
|
|
159
|
-
when :
|
|
160
|
-
|
|
177
|
+
when :none
|
|
178
|
+
cli
|
|
161
179
|
else
|
|
162
|
-
"#{cli}
|
|
180
|
+
"#{cli}=#{value_str}"
|
|
163
181
|
end
|
|
164
182
|
end
|
|
165
183
|
end
|
|
166
184
|
|
|
185
|
+
# Detect assignment delimiter based on CLI prefix
|
|
186
|
+
#
|
|
187
|
+
# @param cli [String] the CLI flag
|
|
188
|
+
# @return [Symbol] the delimiter
|
|
189
|
+
def detect_delimiter(cli)
|
|
190
|
+
case cli
|
|
191
|
+
when /^--/ then :equals # --flag=value
|
|
192
|
+
when /^-/ then :space # -f value
|
|
193
|
+
when %r{^/} then :colon # /format:value
|
|
194
|
+
else :equals
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
167
198
|
# Format a flag
|
|
168
199
|
#
|
|
169
200
|
# @param flag_def [Models::FlagDefinition] the flag definition
|
|
@@ -178,11 +209,38 @@ module Ukiryu
|
|
|
178
209
|
# Build environment variables for command
|
|
179
210
|
#
|
|
180
211
|
# @param command [Models::CommandDefinition] the command definition
|
|
212
|
+
# @param profile [Models::PlatformProfile] the profile (for env_var_sets)
|
|
181
213
|
# @param params [Hash] the parameters hash
|
|
182
214
|
# @return [Hash] the environment variables hash
|
|
183
|
-
def build_env_vars(command, params)
|
|
215
|
+
def build_env_vars(command, profile, params)
|
|
184
216
|
env_vars = {}
|
|
185
217
|
|
|
218
|
+
# First, add env vars from sets specified in use_env_vars
|
|
219
|
+
command.use_env_vars&.each do |set_name|
|
|
220
|
+
set = profile.env_var_sets&.dig(set_name.to_s)
|
|
221
|
+
next unless set
|
|
222
|
+
|
|
223
|
+
set.each do |ev_data|
|
|
224
|
+
# Convert hash to EnvVarDefinition if needed
|
|
225
|
+
ev = ev_data.is_a?(Hash) ? Models::EnvVarDefinition.new(ev_data) : ev_data
|
|
226
|
+
|
|
227
|
+
# Check platform restriction
|
|
228
|
+
platforms = ev.platforms || []
|
|
229
|
+
next if platforms.any? && !platforms.map(&:to_sym).include?(@platform)
|
|
230
|
+
|
|
231
|
+
# Get value - use ev.value if provided, or extract from params
|
|
232
|
+
value = if ev.value
|
|
233
|
+
ev.value
|
|
234
|
+
elsif ev.from
|
|
235
|
+
params[ev.from.to_sym]
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Set the environment variable if value is defined (including empty string)
|
|
239
|
+
env_vars[ev.name] = value.to_s unless value.nil?
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Then, add command's own env_vars (can override set values)
|
|
186
244
|
command.env_vars&.each do |ev|
|
|
187
245
|
# Check platform restriction
|
|
188
246
|
platforms = ev.platforms || []
|
|
@@ -191,8 +249,8 @@ module Ukiryu
|
|
|
191
249
|
# Get value - use ev.value if provided, or extract from params
|
|
192
250
|
value = if ev.value
|
|
193
251
|
ev.value
|
|
194
|
-
elsif ev.
|
|
195
|
-
params[ev.
|
|
252
|
+
elsif ev.from
|
|
253
|
+
params[ev.from.to_sym]
|
|
196
254
|
end
|
|
197
255
|
|
|
198
256
|
# Set the environment variable if value is defined (including empty string)
|
|
@@ -40,9 +40,9 @@ module Ukiryu
|
|
|
40
40
|
load_attributes(EnvSchema.all_output_attributes)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
# Load
|
|
44
|
-
def
|
|
45
|
-
load_attributes(EnvSchema.
|
|
43
|
+
# Load register-specific environment overrides
|
|
44
|
+
def load_register
|
|
45
|
+
load_attributes(EnvSchema.all_register_attributes)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
private
|
|
@@ -17,8 +17,8 @@ module Ukiryu
|
|
|
17
17
|
format: :symbol,
|
|
18
18
|
output: :string,
|
|
19
19
|
|
|
20
|
-
#
|
|
21
|
-
|
|
20
|
+
# Register options
|
|
21
|
+
register: :string,
|
|
22
22
|
|
|
23
23
|
# Tool discovery options
|
|
24
24
|
search_paths: :string, # Comma-separated paths
|
|
@@ -48,14 +48,14 @@ module Ukiryu
|
|
|
48
48
|
%i[format output use_color]
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
# All
|
|
52
|
-
def
|
|
53
|
-
%i[
|
|
51
|
+
# All register attributes
|
|
52
|
+
def all_register_attributes
|
|
53
|
+
%i[register search_paths]
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
# All attributes
|
|
57
57
|
def all_attributes
|
|
58
|
-
%i[timeout debug dry_run metrics shell format output
|
|
58
|
+
%i[timeout debug dry_run metrics shell format output register search_paths use_color]
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
end
|
data/lib/ukiryu/config.rb
CHANGED
|
@@ -129,16 +129,16 @@ module Ukiryu
|
|
|
129
129
|
@resolver.set_programmatic(:output, value)
|
|
130
130
|
end
|
|
131
131
|
|
|
132
|
-
#
|
|
133
|
-
# @return [String, nil] path to tool
|
|
134
|
-
def
|
|
135
|
-
@resolver.resolve(:
|
|
132
|
+
# Register path
|
|
133
|
+
# @return [String, nil] path to tool register
|
|
134
|
+
def register
|
|
135
|
+
@resolver.resolve(:register)
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
-
# Set
|
|
139
|
-
# @param value [String] path to tool
|
|
140
|
-
def
|
|
141
|
-
@resolver.set_programmatic(:
|
|
138
|
+
# Set register path
|
|
139
|
+
# @param value [String] path to tool register
|
|
140
|
+
def register=(value)
|
|
141
|
+
@resolver.set_programmatic(:register, value)
|
|
142
142
|
end
|
|
143
143
|
|
|
144
144
|
# Tool search paths (comma-separated)
|
|
@@ -214,7 +214,7 @@ module Ukiryu
|
|
|
214
214
|
shell: shell,
|
|
215
215
|
format: format,
|
|
216
216
|
output: output,
|
|
217
|
-
|
|
217
|
+
register: register,
|
|
218
218
|
search_paths: search_paths,
|
|
219
219
|
use_color: use_color
|
|
220
220
|
}
|
|
@@ -231,9 +231,9 @@ module Ukiryu
|
|
|
231
231
|
shell: nil,
|
|
232
232
|
format: :yaml,
|
|
233
233
|
output: nil,
|
|
234
|
-
|
|
234
|
+
register: nil,
|
|
235
235
|
search_paths: nil,
|
|
236
|
-
use_color: nil
|
|
236
|
+
use_color: nil
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
env = EnvProvider.load_all
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require_relative 'metadata'
|
|
5
|
+
|
|
6
|
+
module Ukiryu
|
|
7
|
+
module Definition
|
|
8
|
+
# Cache for tool definitions with hot-reload support
|
|
9
|
+
#
|
|
10
|
+
# This class provides caching for tool definitions with automatic
|
|
11
|
+
# invalidation based on file modification time and TTL.
|
|
12
|
+
class DefinitionCache
|
|
13
|
+
# Default cache TTL in seconds (5 minutes)
|
|
14
|
+
DEFAULT_TTL = 300
|
|
15
|
+
|
|
16
|
+
# Cache entry structure
|
|
17
|
+
class Entry
|
|
18
|
+
attr_reader :definition, :mtime, :loaded_at, :source_key
|
|
19
|
+
|
|
20
|
+
def initialize(definition, mtime: nil, source_key: nil)
|
|
21
|
+
@definition = definition
|
|
22
|
+
@mtime = mtime
|
|
23
|
+
@loaded_at = Time.now
|
|
24
|
+
@source_key = source_key || generate_source_key(definition)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Check if entry is stale
|
|
28
|
+
#
|
|
29
|
+
# @param ttl [Integer] time to live in seconds
|
|
30
|
+
# @return [Boolean] true if entry is stale
|
|
31
|
+
def stale?(ttl: DEFAULT_TTL)
|
|
32
|
+
# Check TTL
|
|
33
|
+
return true if Time.now - @loaded_at > ttl
|
|
34
|
+
|
|
35
|
+
# Check mtime for file-based definitions
|
|
36
|
+
if @mtime && @definition.respond_to?(:path)
|
|
37
|
+
path = @definition.path
|
|
38
|
+
return true if path && File.exist?(path) && File.mtime(path) > @mtime
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Refresh the entry
|
|
45
|
+
#
|
|
46
|
+
# @return [Entry] refreshed entry
|
|
47
|
+
def refresh
|
|
48
|
+
if @definition.respond_to?(:load_definition)
|
|
49
|
+
# Reload from metadata
|
|
50
|
+
new_def = @definition.load_definition
|
|
51
|
+
Entry.new(new_def, mtime: new_def.mtime, source_key: @source_key)
|
|
52
|
+
else
|
|
53
|
+
# Can't refresh, return as-is
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def generate_source_key(definition)
|
|
61
|
+
# Generate cache key from definition
|
|
62
|
+
if definition.respond_to?(:name) && definition.respond_to?(:version)
|
|
63
|
+
Digest::SHA256.hexdigest("#{definition.name}/#{definition.version}")
|
|
64
|
+
else
|
|
65
|
+
Digest::SHA256.hexdigest(definition.object_id.to_s)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Singleton instance
|
|
71
|
+
@instance = nil
|
|
72
|
+
|
|
73
|
+
class << self
|
|
74
|
+
# Get the singleton instance
|
|
75
|
+
#
|
|
76
|
+
# @return [DefinitionCache] the cache instance
|
|
77
|
+
def instance
|
|
78
|
+
@instance ||= new
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Reset the singleton (useful for testing)
|
|
82
|
+
def reset_instance
|
|
83
|
+
@instance = nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Initialize a new cache
|
|
88
|
+
#
|
|
89
|
+
# @param ttl [Integer] default time-to-live in seconds
|
|
90
|
+
def initialize(ttl: DEFAULT_TTL)
|
|
91
|
+
@cache = {}
|
|
92
|
+
@ttl = ttl
|
|
93
|
+
@refresh_strategy = :lazy
|
|
94
|
+
@mutex = Mutex.new
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get a cached definition
|
|
98
|
+
#
|
|
99
|
+
# @param key [String] the cache key
|
|
100
|
+
# @return [Models::ToolDefinition, nil] the cached definition, or nil
|
|
101
|
+
def get(key)
|
|
102
|
+
@mutex.synchronize do
|
|
103
|
+
entry = @cache[key]
|
|
104
|
+
return nil unless entry
|
|
105
|
+
|
|
106
|
+
# Check staleness
|
|
107
|
+
if entry.stale?(ttl: @ttl)
|
|
108
|
+
if @refresh_strategy == :lazy
|
|
109
|
+
# Refresh and return
|
|
110
|
+
entry = entry.refresh
|
|
111
|
+
@cache[key] = entry if entry
|
|
112
|
+
else
|
|
113
|
+
# Return stale entry (eager mode refreshes in background)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
entry.definition
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Set a cached definition
|
|
121
|
+
#
|
|
122
|
+
# @param key [String] the cache key
|
|
123
|
+
# @param definition [Models::ToolDefinition] the definition to cache
|
|
124
|
+
# @param metadata [DefinitionMetadata, nil] optional metadata for mtime tracking
|
|
125
|
+
# @return [Models::ToolDefinition] the cached definition
|
|
126
|
+
def set(key, definition, metadata: nil)
|
|
127
|
+
@mutex.synchronize do
|
|
128
|
+
mtime = metadata&.mtime
|
|
129
|
+
entry = Entry.new(definition, mtime: mtime)
|
|
130
|
+
@cache[key] = entry
|
|
131
|
+
definition
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Check if a key exists in cache
|
|
136
|
+
#
|
|
137
|
+
# @param key [String] the cache key
|
|
138
|
+
# @return [Boolean] true if key exists
|
|
139
|
+
def key?(key)
|
|
140
|
+
@cache.key?(key)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Invalidate a cache entry
|
|
144
|
+
#
|
|
145
|
+
# @param key [String] the cache key to invalidate
|
|
146
|
+
# @return [Boolean] true if entry was invalidated
|
|
147
|
+
def invalidate(key)
|
|
148
|
+
@mutex.synchronize do
|
|
149
|
+
!@cache.delete(key).nil?
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Clear all cache entries
|
|
154
|
+
#
|
|
155
|
+
# @return [Integer] number of entries cleared
|
|
156
|
+
def clear
|
|
157
|
+
@mutex.synchronize do
|
|
158
|
+
count = @cache.size
|
|
159
|
+
@cache.clear
|
|
160
|
+
count
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Get cache statistics
|
|
165
|
+
#
|
|
166
|
+
# @return [Hash] cache statistics
|
|
167
|
+
def stats
|
|
168
|
+
{
|
|
169
|
+
size: @cache.size,
|
|
170
|
+
ttl: @ttl,
|
|
171
|
+
refresh_strategy: @refresh_strategy,
|
|
172
|
+
entries: @cache.keys
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Check if a cache entry is stale
|
|
177
|
+
#
|
|
178
|
+
# @param key [String] the cache key
|
|
179
|
+
# @return [Boolean] true if entry is stale
|
|
180
|
+
def stale?(key)
|
|
181
|
+
entry = @cache[key]
|
|
182
|
+
return true unless entry
|
|
183
|
+
|
|
184
|
+
entry.stale?(ttl: @ttl)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Set the refresh strategy
|
|
188
|
+
#
|
|
189
|
+
# @param strategy [Symbol] :lazy or :eager
|
|
190
|
+
def refresh_strategy=(strategy)
|
|
191
|
+
raise ArgumentError, "Invalid refresh strategy: #{strategy}" unless %i[lazy eager never].include?(strategy)
|
|
192
|
+
|
|
193
|
+
@refresh_strategy = strategy
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Get the current refresh strategy
|
|
197
|
+
#
|
|
198
|
+
# @return [Symbol] the refresh strategy
|
|
199
|
+
attr_reader :refresh_strategy
|
|
200
|
+
|
|
201
|
+
# Set the cache TTL
|
|
202
|
+
#
|
|
203
|
+
# @param ttl [Integer] time-to-live in seconds
|
|
204
|
+
|
|
205
|
+
# Get the current TTL
|
|
206
|
+
#
|
|
207
|
+
# @return [Integer] the TTL in seconds
|
|
208
|
+
attr_accessor :ttl
|
|
209
|
+
|
|
210
|
+
# Refresh all stale entries
|
|
211
|
+
#
|
|
212
|
+
# @return [Integer] number of entries refreshed
|
|
213
|
+
def refresh_stale
|
|
214
|
+
@mutex.synchronize do
|
|
215
|
+
count = 0
|
|
216
|
+
@cache.each do |key, entry|
|
|
217
|
+
if entry.stale?(ttl: @ttl)
|
|
218
|
+
@cache[key] = entry.refresh
|
|
219
|
+
count += 1
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
count
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Prune stale entries
|
|
227
|
+
#
|
|
228
|
+
# @return [Integer] number of entries pruned
|
|
229
|
+
def prune
|
|
230
|
+
@mutex.synchronize do
|
|
231
|
+
stale_keys = @cache.select { |_, entry| entry.stale?(ttl: @ttl) }.keys
|
|
232
|
+
stale_keys.each { |key| @cache.delete(key) }
|
|
233
|
+
stale_keys.size
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|