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
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'source'
|
|
4
|
+
require_relative 'sources/file'
|
|
5
|
+
require_relative 'sources/string'
|
|
6
|
+
require_relative '../register'
|
|
7
|
+
require_relative '../models/tool_definition'
|
|
8
|
+
|
|
9
|
+
module Ukiryu
|
|
10
|
+
module Definition
|
|
11
|
+
# Loader for tool definitions from various sources
|
|
12
|
+
#
|
|
13
|
+
# The loader orchestrates loading tool definitions from different
|
|
14
|
+
# sources (files, strings, bundled locations, register) and provides
|
|
15
|
+
# a unified interface for definition loading.
|
|
16
|
+
class Loader
|
|
17
|
+
class << self
|
|
18
|
+
# Load a tool definition from a source
|
|
19
|
+
#
|
|
20
|
+
# @param source [Source] the definition source
|
|
21
|
+
# @param options [Hash] loading options
|
|
22
|
+
# @option options [Symbol] :validation validation mode (:strict, :lenient, :none)
|
|
23
|
+
# @return [Models::ToolDefinition] the loaded tool definition
|
|
24
|
+
# @raise [DefinitionLoadError] if loading fails
|
|
25
|
+
def load_from_source(source, options = {})
|
|
26
|
+
# Check cache first
|
|
27
|
+
cache_key = source.cache_key
|
|
28
|
+
return profile_cache[cache_key] if profile_cache.key?(cache_key)
|
|
29
|
+
|
|
30
|
+
# Load YAML content from source
|
|
31
|
+
yaml_content = source.load
|
|
32
|
+
|
|
33
|
+
# Parse using lutaml-model
|
|
34
|
+
profile = parse_yaml(yaml_content, source)
|
|
35
|
+
|
|
36
|
+
# Validate if requested
|
|
37
|
+
validation_mode = options[:validation] || :strict
|
|
38
|
+
validate_profile(profile, validation_mode) if validation_mode != :none
|
|
39
|
+
|
|
40
|
+
# Cache the profile
|
|
41
|
+
profile_cache[cache_key] = profile
|
|
42
|
+
|
|
43
|
+
profile
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Load a tool definition from a file path
|
|
47
|
+
#
|
|
48
|
+
# @param path [String] path to the definition file
|
|
49
|
+
# @param options [Hash] loading options
|
|
50
|
+
# @return [Models::ToolDefinition] the loaded tool definition
|
|
51
|
+
def load_from_file(path, options = {})
|
|
52
|
+
source = Sources::FileSource.new(path)
|
|
53
|
+
load_from_source(source, options)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Load a tool definition from a YAML string
|
|
57
|
+
#
|
|
58
|
+
# @param yaml_string [String] the YAML content
|
|
59
|
+
# @param options [Hash] loading options
|
|
60
|
+
# @return [Models::ToolDefinition] the loaded tool definition
|
|
61
|
+
def load_from_string(yaml_string, options = {})
|
|
62
|
+
source = Sources::StringSource.new(yaml_string)
|
|
63
|
+
load_from_source(source, options)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get the profile cache
|
|
67
|
+
#
|
|
68
|
+
# @return [Hash] the profile cache
|
|
69
|
+
def profile_cache
|
|
70
|
+
@profile_cache ||= {}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Clear the profile cache
|
|
74
|
+
#
|
|
75
|
+
# @param source [Source, nil] clear specific source or all if nil
|
|
76
|
+
def clear_cache(source = nil)
|
|
77
|
+
if source
|
|
78
|
+
profile_cache.delete(source.cache_key)
|
|
79
|
+
else
|
|
80
|
+
profile_cache.clear
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Parse YAML content into a tool definition
|
|
87
|
+
#
|
|
88
|
+
# @param yaml_content [String] the YAML content
|
|
89
|
+
# @param source [Source] the source for error messages
|
|
90
|
+
# @return [Models::ToolDefinition] the parsed profile
|
|
91
|
+
# @raise [DefinitionLoadError] if parsing fails
|
|
92
|
+
def parse_yaml(yaml_content, source)
|
|
93
|
+
Models::ToolDefinition.from_yaml(yaml_content)
|
|
94
|
+
rescue Psych::SyntaxError => e
|
|
95
|
+
raise DefinitionLoadError,
|
|
96
|
+
"Invalid YAML in #{source}: #{e.message}"
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
raise DefinitionLoadError,
|
|
99
|
+
"Failed to parse definition from #{source}: #{e.message}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Validate a tool profile
|
|
103
|
+
#
|
|
104
|
+
# @param profile [Models::ToolDefinition] the profile to validate
|
|
105
|
+
# @param mode [Symbol] validation mode (:strict, :lenient)
|
|
106
|
+
# @raise [DefinitionValidationError] if validation fails in strict mode
|
|
107
|
+
def validate_profile(profile, mode)
|
|
108
|
+
errors = []
|
|
109
|
+
|
|
110
|
+
# Check required fields
|
|
111
|
+
errors << "Missing 'name' field" unless profile.name
|
|
112
|
+
errors << "Missing 'version' field" unless profile.version
|
|
113
|
+
errors << "Missing 'profiles' field or profiles is empty" unless profile.profiles&.any?
|
|
114
|
+
|
|
115
|
+
# Check ukiryu_schema format if present
|
|
116
|
+
errors << "Invalid ukiryu_schema format: #{profile.ukiryu_schema}" if profile.ukiryu_schema && !profile.ukiryu_schema.match?(/^\d+\.\d+$/)
|
|
117
|
+
|
|
118
|
+
# Check $self URI format if present
|
|
119
|
+
errors << "Invalid $self URI format: #{profile.self_uri}" if profile.self_uri && !valid_uri?(profile.self_uri) && (mode == :strict)
|
|
120
|
+
|
|
121
|
+
return if errors.empty?
|
|
122
|
+
|
|
123
|
+
message = "Profile validation failed:\n - #{errors.join("\n - ")}"
|
|
124
|
+
raise DefinitionValidationError, message if mode == :strict
|
|
125
|
+
|
|
126
|
+
warn "[Ukiryu] #{message}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Check if a string is a valid URI
|
|
130
|
+
#
|
|
131
|
+
# @param uri_string [String] the URI to check
|
|
132
|
+
# @return [Boolean] true if valid URI
|
|
133
|
+
def valid_uri?(uri_string)
|
|
134
|
+
uri_string =~ %r{^https?://} || uri_string =~ %r{^file://} ? true : false
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../models/tool_definition'
|
|
4
|
+
|
|
5
|
+
module Ukiryu
|
|
6
|
+
module Definition
|
|
7
|
+
# Metadata about a discovered tool definition
|
|
8
|
+
#
|
|
9
|
+
# This class encapsulates information about a tool definition
|
|
10
|
+
# that was discovered in the filesystem.
|
|
11
|
+
class DefinitionMetadata
|
|
12
|
+
include Comparable
|
|
13
|
+
|
|
14
|
+
# The tool name
|
|
15
|
+
# @return [String] tool name
|
|
16
|
+
attr_reader :name
|
|
17
|
+
|
|
18
|
+
# The tool version
|
|
19
|
+
# @return [String] tool version
|
|
20
|
+
attr_reader :version
|
|
21
|
+
|
|
22
|
+
# The path to the definition file
|
|
23
|
+
# @return [String] absolute path to the YAML file
|
|
24
|
+
attr_reader :path
|
|
25
|
+
|
|
26
|
+
# The source type (user, system, bundled, register)
|
|
27
|
+
# @return [Symbol] source type
|
|
28
|
+
attr_reader :source_type
|
|
29
|
+
|
|
30
|
+
# Create a new definition metadata
|
|
31
|
+
#
|
|
32
|
+
# @param name [String] tool name
|
|
33
|
+
# @param version [String] tool version
|
|
34
|
+
# @param path [String] path to the definition file
|
|
35
|
+
# @param source_type [Symbol] source type
|
|
36
|
+
def initialize(name:, version:, path:, source_type:)
|
|
37
|
+
@name = name
|
|
38
|
+
@version = version
|
|
39
|
+
@path = File.expand_path(path)
|
|
40
|
+
@source_type = source_type
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Get the tool definition by loading the YAML
|
|
44
|
+
#
|
|
45
|
+
# @return [Models::ToolDefinition] the loaded tool definition
|
|
46
|
+
# @raise [DefinitionLoadError] if loading fails
|
|
47
|
+
def load_definition
|
|
48
|
+
Loader.load_from_file(@path)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Check if the definition file exists
|
|
52
|
+
#
|
|
53
|
+
# @return [Boolean] true if file exists
|
|
54
|
+
def exists?
|
|
55
|
+
File.exist?(@path)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get the file modification time
|
|
59
|
+
#
|
|
60
|
+
# @return [Time, nil] file mtime, or nil if file doesn't exist
|
|
61
|
+
def mtime
|
|
62
|
+
File.mtime(@path) if exists?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Get a string representation
|
|
66
|
+
#
|
|
67
|
+
# @return [String] string representation
|
|
68
|
+
def to_s
|
|
69
|
+
"#{@name}/#{@version} (#{@source_type}) - #{@path}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Detailed inspection string
|
|
73
|
+
#
|
|
74
|
+
# @return [String] inspection string
|
|
75
|
+
def inspect
|
|
76
|
+
"#<#{self.class.name} name=#{@name.inspect} version=#{@version.inspect} path=#{@path.inspect} source_type=#{@source_type.inspect}>"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Compare two metadata objects
|
|
80
|
+
#
|
|
81
|
+
# @param other [DefinitionMetadata] the other metadata
|
|
82
|
+
# @return [Boolean] true if equal
|
|
83
|
+
def ==(other)
|
|
84
|
+
return false unless other.is_a?(DefinitionMetadata)
|
|
85
|
+
|
|
86
|
+
@name == other.name &&
|
|
87
|
+
@version == other.version &&
|
|
88
|
+
@path == other.path &&
|
|
89
|
+
@source_type == other.source_type
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Hash code for hash keys
|
|
93
|
+
#
|
|
94
|
+
# @return [Integer] hash code
|
|
95
|
+
def hash
|
|
96
|
+
[@name, @version, @path, @source_type].hash
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Source type priorities (lower = higher priority)
|
|
100
|
+
#
|
|
101
|
+
# @return [Integer] priority value
|
|
102
|
+
def priority
|
|
103
|
+
case @source_type
|
|
104
|
+
when :user then 1
|
|
105
|
+
when :bundled then 2
|
|
106
|
+
when :local_system then 3
|
|
107
|
+
when :system then 4
|
|
108
|
+
when :register then 5
|
|
109
|
+
else 999
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Compare priorities for sorting
|
|
114
|
+
#
|
|
115
|
+
# @param other [DefinitionMetadata] the other metadata
|
|
116
|
+
# @return [Integer] comparison result
|
|
117
|
+
def <=>(other)
|
|
118
|
+
return 0 unless other.is_a?(DefinitionMetadata)
|
|
119
|
+
|
|
120
|
+
# Compare by priority (lower number = higher priority)
|
|
121
|
+
priority_comparison = priority <=> other.priority
|
|
122
|
+
return priority_comparison unless priority_comparison.zero?
|
|
123
|
+
|
|
124
|
+
# Same priority, compare by version (descending - higher version first)
|
|
125
|
+
version_comparison = compare_versions(@version, other.version)
|
|
126
|
+
return version_comparison unless version_comparison.zero?
|
|
127
|
+
|
|
128
|
+
# Same version, compare by name
|
|
129
|
+
@name <=> other.name
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
# Compare version strings
|
|
135
|
+
#
|
|
136
|
+
# Returns result for descending order (higher version first)
|
|
137
|
+
#
|
|
138
|
+
# @param v1 [String] first version
|
|
139
|
+
# @param v2 [String] second version
|
|
140
|
+
# @return [Integer] comparison result (negated for descending)
|
|
141
|
+
def compare_versions(v1, v2)
|
|
142
|
+
# Simple version comparison - can be enhanced later
|
|
143
|
+
parts1 = v1.split('.').map(&:to_i)
|
|
144
|
+
parts2 = v2.split('.').map(&:to_i)
|
|
145
|
+
|
|
146
|
+
max_length = [parts1.length, parts2.length].max
|
|
147
|
+
max_length.times do |i|
|
|
148
|
+
p1 = parts1[i] || 0
|
|
149
|
+
p2 = parts2[i] || 0
|
|
150
|
+
comparison = p1 <=> p2
|
|
151
|
+
# Negate for descending order (higher version = lower value)
|
|
152
|
+
return -comparison unless comparison.zero?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
0
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ukiryu
|
|
4
|
+
module Definition
|
|
5
|
+
# Abstract base class for definition sources
|
|
6
|
+
#
|
|
7
|
+
# A source represents a location from which a tool definition
|
|
8
|
+
# can be loaded. Each source type (file, string, bundled, register)
|
|
9
|
+
# implements this interface.
|
|
10
|
+
#
|
|
11
|
+
# @abstract Subclasses must implement {#load}, {#cache_key}, and {#source_type}
|
|
12
|
+
class Source
|
|
13
|
+
# Load the YAML definition content
|
|
14
|
+
#
|
|
15
|
+
# @abstract
|
|
16
|
+
# @return [String] the YAML content
|
|
17
|
+
# @raise [DefinitionLoadError] if the definition cannot be loaded
|
|
18
|
+
def load
|
|
19
|
+
raise NotImplementedError, "#{self.class} must implement #load"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Get a unique cache key for this source
|
|
23
|
+
#
|
|
24
|
+
# The cache key must uniquely identify both the source location
|
|
25
|
+
# and its content to ensure proper cache invalidation.
|
|
26
|
+
#
|
|
27
|
+
# @abstract
|
|
28
|
+
# @return [String] a unique cache key
|
|
29
|
+
def cache_key
|
|
30
|
+
raise NotImplementedError, "#{self.class} must implement #cache_key"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Get the source type identifier
|
|
34
|
+
#
|
|
35
|
+
# @abstract
|
|
36
|
+
# @return [Symbol] the source type (:file, :string, :bundled, :register)
|
|
37
|
+
def source_type
|
|
38
|
+
raise NotImplementedError, "#{self.class} must implement #source_type"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Check if this source is equal to another
|
|
42
|
+
#
|
|
43
|
+
# Two sources are equal if they have the same cache key.
|
|
44
|
+
#
|
|
45
|
+
# @param other [Object] the object to compare
|
|
46
|
+
# @return [Boolean] true if sources are equal
|
|
47
|
+
def ==(other)
|
|
48
|
+
return false unless other.is_a?(Source)
|
|
49
|
+
|
|
50
|
+
cache_key == other.cache_key
|
|
51
|
+
end
|
|
52
|
+
alias eql? ==
|
|
53
|
+
|
|
54
|
+
# Generate hash code for hash storage
|
|
55
|
+
#
|
|
56
|
+
# @return [Integer] hash code based on cache key
|
|
57
|
+
def hash
|
|
58
|
+
cache_key.hash
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# String representation
|
|
62
|
+
#
|
|
63
|
+
# @return [String] source description
|
|
64
|
+
def to_s
|
|
65
|
+
"#{source_type}:#{cache_key}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Inspect representation
|
|
69
|
+
#
|
|
70
|
+
# @return [String] detailed inspection string
|
|
71
|
+
def inspect
|
|
72
|
+
"#<#{self.class.name} source_type=#{source_type} cache_key=#{cache_key}>"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
protected
|
|
76
|
+
|
|
77
|
+
# Calculate SHA256 hash of a string
|
|
78
|
+
#
|
|
79
|
+
# @param string [String] the string to hash
|
|
80
|
+
# @return [String] hexadecimal SHA256 hash
|
|
81
|
+
def sha256(string)
|
|
82
|
+
require 'digest'
|
|
83
|
+
Digest::SHA256.hexdigest(string)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../source'
|
|
4
|
+
|
|
5
|
+
module Ukiryu
|
|
6
|
+
module Definition
|
|
7
|
+
module Sources
|
|
8
|
+
# Load tool definitions from a file
|
|
9
|
+
#
|
|
10
|
+
# This source reads YAML tool definitions from the filesystem.
|
|
11
|
+
# It tracks file metadata for cache invalidation.
|
|
12
|
+
class FileSource < Source
|
|
13
|
+
# The path to the definition file
|
|
14
|
+
# @return [String] absolute path to the file
|
|
15
|
+
attr_reader :path
|
|
16
|
+
|
|
17
|
+
# The file modification time
|
|
18
|
+
# @return [Time] file mtime
|
|
19
|
+
attr_reader :mtime
|
|
20
|
+
|
|
21
|
+
# Create a new file-based definition source
|
|
22
|
+
#
|
|
23
|
+
# @param path [String] path to the definition file
|
|
24
|
+
# @raise [DefinitionNotFoundError] if the file doesn't exist
|
|
25
|
+
# @raise [DefinitionLoadError] if the file is not readable
|
|
26
|
+
def initialize(path)
|
|
27
|
+
@path = expand_path(path)
|
|
28
|
+
@mtime = nil # Will be set on first load
|
|
29
|
+
@cached_content = nil
|
|
30
|
+
|
|
31
|
+
validate_file_exists!
|
|
32
|
+
validate_file_readable!
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Load the YAML definition content from the file
|
|
36
|
+
#
|
|
37
|
+
# @return [String] the YAML content
|
|
38
|
+
# @raise [DefinitionLoadError] if the file cannot be read
|
|
39
|
+
def load
|
|
40
|
+
current_mtime = get_mtime
|
|
41
|
+
|
|
42
|
+
# Check if file has changed since init
|
|
43
|
+
if @mtime && @mtime != current_mtime
|
|
44
|
+
raise DefinitionLoadError,
|
|
45
|
+
"File #{@path} has been modified since it was loaded. " \
|
|
46
|
+
"Original mtime: #{@mtime}, Current mtime: #{current_mtime}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@mtime = current_mtime
|
|
50
|
+
|
|
51
|
+
# Read and cache content
|
|
52
|
+
@load ||= read_file
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Get the source type
|
|
56
|
+
#
|
|
57
|
+
# @return [Symbol] :file
|
|
58
|
+
def source_type
|
|
59
|
+
:file
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get a unique cache key for this file source
|
|
63
|
+
#
|
|
64
|
+
# The cache key includes a hash of the path and the mtime,
|
|
65
|
+
# ensuring that file changes invalidate the cache.
|
|
66
|
+
#
|
|
67
|
+
# @return [String] unique cache key
|
|
68
|
+
def cache_key
|
|
69
|
+
"file:#{sha256(path)}:#{mtime || get_mtime}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get the real path (resolves symlinks)
|
|
73
|
+
#
|
|
74
|
+
# @return [String] real path to the file
|
|
75
|
+
def real_path
|
|
76
|
+
@real_path ||= File.realpath(path)
|
|
77
|
+
rescue Errno::ENOENT
|
|
78
|
+
path
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Expand the path to an absolute path
|
|
84
|
+
#
|
|
85
|
+
# @param path [String] the path to expand
|
|
86
|
+
# @return [String] absolute path
|
|
87
|
+
def expand_path(path)
|
|
88
|
+
File.expand_path(path)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Validate that the file exists
|
|
92
|
+
#
|
|
93
|
+
# @raise [DefinitionNotFoundError] if file doesn't exist
|
|
94
|
+
def validate_file_exists!
|
|
95
|
+
return if File.exist?(path)
|
|
96
|
+
|
|
97
|
+
raise DefinitionNotFoundError,
|
|
98
|
+
"Definition file not found: #{path}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Validate that the file is readable
|
|
102
|
+
#
|
|
103
|
+
# @raise [DefinitionLoadError] if file is not readable
|
|
104
|
+
def validate_file_readable!
|
|
105
|
+
return if File.readable?(path)
|
|
106
|
+
|
|
107
|
+
raise DefinitionLoadError,
|
|
108
|
+
"Definition file is not readable: #{path}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Get the file modification time
|
|
112
|
+
#
|
|
113
|
+
# @return [Time] file mtime
|
|
114
|
+
# @raise [DefinitionLoadError] if mtime cannot be determined
|
|
115
|
+
def get_mtime
|
|
116
|
+
File.mtime(path)
|
|
117
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
118
|
+
raise DefinitionLoadError,
|
|
119
|
+
"Cannot access file metadata for #{path}: #{e.message}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Read the file content
|
|
123
|
+
#
|
|
124
|
+
# @return [String] file content
|
|
125
|
+
# @raise [DefinitionLoadError] if file cannot be read
|
|
126
|
+
def read_file
|
|
127
|
+
File.read(path)
|
|
128
|
+
rescue Errno::EACCES => e
|
|
129
|
+
raise DefinitionLoadError,
|
|
130
|
+
"Permission denied reading file #{path}: #{e.message}"
|
|
131
|
+
rescue IOError, SystemCallError => e
|
|
132
|
+
raise DefinitionLoadError,
|
|
133
|
+
"Error reading file #{path}: #{e.message}"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../source'
|
|
4
|
+
|
|
5
|
+
module Ukiryu
|
|
6
|
+
module Definition
|
|
7
|
+
module Sources
|
|
8
|
+
# Load tool definitions from a YAML string
|
|
9
|
+
#
|
|
10
|
+
# This source handles YAML content provided directly as a string,
|
|
11
|
+
# useful for definitions obtained via command-line flags or
|
|
12
|
+
# programmatic generation.
|
|
13
|
+
class StringSource < Source
|
|
14
|
+
# The YAML content
|
|
15
|
+
# @return [String] the YAML string
|
|
16
|
+
attr_reader :content
|
|
17
|
+
|
|
18
|
+
# The SHA256 hash of the content
|
|
19
|
+
# @return [String] hexadecimal hash
|
|
20
|
+
attr_reader :content_hash
|
|
21
|
+
|
|
22
|
+
# Create a new string-based definition source
|
|
23
|
+
#
|
|
24
|
+
# @param content [String] the YAML content
|
|
25
|
+
# @raise [ArgumentError] if content is not a String
|
|
26
|
+
# @raise [DefinitionLoadError] if content is empty
|
|
27
|
+
def initialize(content)
|
|
28
|
+
@content = validate_content!(content)
|
|
29
|
+
@content_hash = sha256(@content)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Load the YAML definition content
|
|
33
|
+
#
|
|
34
|
+
# @return [String] the YAML content
|
|
35
|
+
def load
|
|
36
|
+
@content
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get the source type
|
|
40
|
+
#
|
|
41
|
+
# @return [Symbol] :string
|
|
42
|
+
def source_type
|
|
43
|
+
:string
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get a unique cache key for this string source
|
|
47
|
+
#
|
|
48
|
+
# The cache key is based on the SHA256 hash of the content,
|
|
49
|
+
# ensuring identical strings produce identical cache keys.
|
|
50
|
+
#
|
|
51
|
+
# @return [String] unique cache key
|
|
52
|
+
def cache_key
|
|
53
|
+
"string:#{content_hash}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Get the size of the content in bytes
|
|
57
|
+
#
|
|
58
|
+
# @return [Integer] content size
|
|
59
|
+
def size
|
|
60
|
+
@content.bytesize
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Validate the content
|
|
66
|
+
#
|
|
67
|
+
# @param content [String] the content to validate
|
|
68
|
+
# @return [String] the validated content
|
|
69
|
+
# @raise [ArgumentError] if content is not a String
|
|
70
|
+
# @raise [DefinitionLoadError] if content is empty
|
|
71
|
+
def validate_content!(content)
|
|
72
|
+
unless content.is_a?(String)
|
|
73
|
+
raise ArgumentError,
|
|
74
|
+
"Definition content must be a String, got #{content.class}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
if content.empty?
|
|
78
|
+
raise DefinitionLoadError,
|
|
79
|
+
'Definition content cannot be empty'
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Strip leading/trailing whitespace but preserve internal formatting
|
|
83
|
+
content.strip
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|