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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +58 -14
  3. data/.gitignore +3 -0
  4. data/.rubocop_todo.yml +170 -79
  5. data/Gemfile +1 -1
  6. data/README.adoc +1603 -576
  7. data/docs/.gitignore +1 -0
  8. data/docs/Gemfile +10 -0
  9. data/docs/INDEX.adoc +261 -0
  10. data/docs/_config.yml +180 -0
  11. data/docs/advanced/custom-tool-classes.adoc +581 -0
  12. data/docs/advanced/index.adoc +20 -0
  13. data/docs/features/configuration.adoc +657 -0
  14. data/docs/features/index.adoc +31 -0
  15. data/docs/features/platform-support.adoc +488 -0
  16. data/docs/getting-started/core-concepts.adoc +666 -0
  17. data/docs/getting-started/index.adoc +36 -0
  18. data/docs/getting-started/installation.adoc +216 -0
  19. data/docs/getting-started/quick-start.adoc +258 -0
  20. data/docs/guides/env-var-sets.adoc +388 -0
  21. data/docs/guides/index.adoc +20 -0
  22. data/docs/interfaces/cli.adoc +609 -0
  23. data/docs/interfaces/index.adoc +153 -0
  24. data/docs/interfaces/ruby-api.adoc +538 -0
  25. data/docs/lychee.toml +49 -0
  26. data/docs/reference/configuration-options.adoc +720 -0
  27. data/docs/reference/error-codes.adoc +634 -0
  28. data/docs/reference/index.adoc +20 -0
  29. data/docs/reference/ruby-api.adoc +1217 -0
  30. data/docs/understanding/index.adoc +20 -0
  31. data/lib/ukiryu/cli.rb +43 -58
  32. data/lib/ukiryu/cli_commands/base_command.rb +16 -27
  33. data/lib/ukiryu/cli_commands/cache_command.rb +100 -0
  34. data/lib/ukiryu/cli_commands/commands_command.rb +8 -8
  35. data/lib/ukiryu/cli_commands/commands_command.rb.fixed +1 -1
  36. data/lib/ukiryu/cli_commands/config_command.rb +49 -7
  37. data/lib/ukiryu/cli_commands/definitions_command.rb +254 -0
  38. data/lib/ukiryu/cli_commands/describe_command.rb +13 -7
  39. data/lib/ukiryu/cli_commands/describe_command.rb.fixed +1 -1
  40. data/lib/ukiryu/cli_commands/docs_command.rb +148 -0
  41. data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +1 -1
  42. data/lib/ukiryu/cli_commands/extract_command.rb +2 -2
  43. data/lib/ukiryu/cli_commands/info_command.rb +7 -7
  44. data/lib/ukiryu/cli_commands/lint_command.rb +167 -0
  45. data/lib/ukiryu/cli_commands/list_command.rb +6 -6
  46. data/lib/ukiryu/cli_commands/opts_command.rb +2 -2
  47. data/lib/ukiryu/cli_commands/opts_command.rb.fixed +1 -1
  48. data/lib/ukiryu/cli_commands/register_command.rb +144 -0
  49. data/lib/ukiryu/cli_commands/resolve_command.rb +124 -0
  50. data/lib/ukiryu/cli_commands/run_command.rb +38 -14
  51. data/lib/ukiryu/cli_commands/run_file_command.rb +2 -2
  52. data/lib/ukiryu/cli_commands/system_command.rb +50 -32
  53. data/lib/ukiryu/cli_commands/validate_command.rb +452 -51
  54. data/lib/ukiryu/cli_commands/which_command.rb +5 -5
  55. data/lib/ukiryu/command_builder.rb +81 -23
  56. data/lib/ukiryu/config/env_provider.rb +3 -3
  57. data/lib/ukiryu/config/env_schema.rb +6 -6
  58. data/lib/ukiryu/config.rb +11 -11
  59. data/lib/ukiryu/definition/definition_cache.rb +238 -0
  60. data/lib/ukiryu/definition/definition_composer.rb +257 -0
  61. data/lib/ukiryu/definition/definition_linter.rb +460 -0
  62. data/lib/ukiryu/definition/definition_validator.rb +320 -0
  63. data/lib/ukiryu/definition/discovery.rb +239 -0
  64. data/lib/ukiryu/definition/documentation_generator.rb +429 -0
  65. data/lib/ukiryu/definition/lint_issue.rb +168 -0
  66. data/lib/ukiryu/definition/loader.rb +139 -0
  67. data/lib/ukiryu/definition/metadata.rb +159 -0
  68. data/lib/ukiryu/definition/source.rb +87 -0
  69. data/lib/ukiryu/definition/sources/file.rb +138 -0
  70. data/lib/ukiryu/definition/sources/string.rb +88 -0
  71. data/lib/ukiryu/definition/validation_result.rb +158 -0
  72. data/lib/ukiryu/definition/version_resolver.rb +194 -0
  73. data/lib/ukiryu/definition.rb +40 -0
  74. data/lib/ukiryu/errors.rb +6 -0
  75. data/lib/ukiryu/execution_context.rb +11 -11
  76. data/lib/ukiryu/executor.rb +6 -0
  77. data/lib/ukiryu/extractors/extractor.rb +6 -5
  78. data/lib/ukiryu/extractors/help_parser.rb +13 -19
  79. data/lib/ukiryu/logger.rb +3 -1
  80. data/lib/ukiryu/models/command_definition.rb +3 -3
  81. data/lib/ukiryu/models/command_info.rb +1 -1
  82. data/lib/ukiryu/models/components.rb +1 -3
  83. data/lib/ukiryu/models/env_var_definition.rb +11 -3
  84. data/lib/ukiryu/models/flag_definition.rb +15 -0
  85. data/lib/ukiryu/models/option_definition.rb +7 -7
  86. data/lib/ukiryu/models/platform_profile.rb +6 -3
  87. data/lib/ukiryu/models/routing.rb +1 -1
  88. data/lib/ukiryu/models/tool_definition.rb +2 -4
  89. data/lib/ukiryu/models/tool_metadata.rb +6 -6
  90. data/lib/ukiryu/models/validation_result.rb +1 -1
  91. data/lib/ukiryu/models/version_compatibility.rb +6 -3
  92. data/lib/ukiryu/models/version_detection.rb +10 -1
  93. data/lib/ukiryu/{registry.rb → register.rb} +54 -38
  94. data/lib/ukiryu/register_auto_manager.rb +268 -0
  95. data/lib/ukiryu/schema_validator.rb +31 -10
  96. data/lib/ukiryu/shell/base.rb +18 -0
  97. data/lib/ukiryu/shell/bash.rb +19 -1
  98. data/lib/ukiryu/shell/cmd.rb +11 -1
  99. data/lib/ukiryu/shell/powershell.rb +11 -1
  100. data/lib/ukiryu/shell.rb +1 -1
  101. data/lib/ukiryu/tool.rb +107 -95
  102. data/lib/ukiryu/tool_index.rb +22 -22
  103. data/lib/ukiryu/tools/base.rb +12 -25
  104. data/lib/ukiryu/tools/generator.rb +7 -7
  105. data/lib/ukiryu/tools.rb +3 -3
  106. data/lib/ukiryu/type.rb +20 -5
  107. data/lib/ukiryu/version.rb +1 -1
  108. data/lib/ukiryu/version_detector.rb +21 -2
  109. data/lib/ukiryu.rb +6 -3
  110. data/ukiryu-proposal.md +41 -41
  111. data/ukiryu.gemspec +1 -0
  112. metadata +64 -8
  113. 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