ukiryu 0.1.0 → 0.1.1

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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +63 -0
  3. data/.github/workflows/links.yml +99 -0
  4. data/.github/workflows/rake.yml +19 -0
  5. data/.github/workflows/release.yml +27 -0
  6. data/.gitignore +18 -4
  7. data/.rubocop.yml +1 -0
  8. data/.rubocop_todo.yml +213 -0
  9. data/Gemfile +12 -8
  10. data/README.adoc +613 -0
  11. data/Rakefile +2 -2
  12. data/docs/assets/logo.svg +1 -0
  13. data/exe/ukiryu +11 -0
  14. data/lib/ukiryu/action/base.rb +77 -0
  15. data/lib/ukiryu/cache.rb +199 -0
  16. data/lib/ukiryu/cli.rb +133 -307
  17. data/lib/ukiryu/cli_commands/base_command.rb +155 -0
  18. data/lib/ukiryu/cli_commands/commands_command.rb +120 -0
  19. data/lib/ukiryu/cli_commands/commands_command.rb.fixed +40 -0
  20. data/lib/ukiryu/cli_commands/config_command.rb +249 -0
  21. data/lib/ukiryu/cli_commands/describe_command.rb +326 -0
  22. data/lib/ukiryu/cli_commands/describe_command.rb.fixed +254 -0
  23. data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +180 -0
  24. data/lib/ukiryu/cli_commands/extract_command.rb +84 -0
  25. data/lib/ukiryu/cli_commands/info_command.rb +156 -0
  26. data/lib/ukiryu/cli_commands/list_command.rb +70 -0
  27. data/lib/ukiryu/cli_commands/opts_command.rb +106 -0
  28. data/lib/ukiryu/cli_commands/opts_command.rb.fixed +105 -0
  29. data/lib/ukiryu/cli_commands/response_formatter.rb +240 -0
  30. data/lib/ukiryu/cli_commands/run_command.rb +375 -0
  31. data/lib/ukiryu/cli_commands/run_file_command.rb +215 -0
  32. data/lib/ukiryu/cli_commands/system_command.rb +90 -0
  33. data/lib/ukiryu/cli_commands/validate_command.rb +87 -0
  34. data/lib/ukiryu/cli_commands/version_command.rb +16 -0
  35. data/lib/ukiryu/cli_commands/which_command.rb +166 -0
  36. data/lib/ukiryu/command_builder.rb +205 -0
  37. data/lib/ukiryu/config/env_provider.rb +64 -0
  38. data/lib/ukiryu/config/env_schema.rb +63 -0
  39. data/lib/ukiryu/config/override_resolver.rb +68 -0
  40. data/lib/ukiryu/config/type_converter.rb +59 -0
  41. data/lib/ukiryu/config.rb +249 -0
  42. data/lib/ukiryu/errors.rb +3 -0
  43. data/lib/ukiryu/executable_locator.rb +114 -0
  44. data/lib/ukiryu/execution/command_info.rb +64 -0
  45. data/lib/ukiryu/execution/metadata.rb +97 -0
  46. data/lib/ukiryu/execution/output.rb +144 -0
  47. data/lib/ukiryu/execution/result.rb +194 -0
  48. data/lib/ukiryu/execution.rb +15 -0
  49. data/lib/ukiryu/execution_context.rb +251 -0
  50. data/lib/ukiryu/executor.rb +76 -493
  51. data/lib/ukiryu/extractors/base_extractor.rb +63 -0
  52. data/lib/ukiryu/extractors/extractor.rb +150 -0
  53. data/lib/ukiryu/extractors/help_parser.rb +188 -0
  54. data/lib/ukiryu/extractors/native_extractor.rb +47 -0
  55. data/lib/ukiryu/io.rb +196 -0
  56. data/lib/ukiryu/logger.rb +544 -0
  57. data/lib/ukiryu/models/argument.rb +28 -0
  58. data/lib/ukiryu/models/argument_definition.rb +119 -0
  59. data/lib/ukiryu/models/arguments.rb +113 -0
  60. data/lib/ukiryu/models/command_definition.rb +176 -0
  61. data/lib/ukiryu/models/command_info.rb +37 -0
  62. data/lib/ukiryu/models/components.rb +107 -0
  63. data/lib/ukiryu/models/env_var_definition.rb +30 -0
  64. data/lib/ukiryu/models/error_response.rb +41 -0
  65. data/lib/ukiryu/models/execution_metadata.rb +31 -0
  66. data/lib/ukiryu/models/execution_report.rb +236 -0
  67. data/lib/ukiryu/models/exit_codes.rb +74 -0
  68. data/lib/ukiryu/models/flag_definition.rb +67 -0
  69. data/lib/ukiryu/models/option_definition.rb +102 -0
  70. data/lib/ukiryu/models/output_info.rb +25 -0
  71. data/lib/ukiryu/models/platform_profile.rb +153 -0
  72. data/lib/ukiryu/models/routing.rb +211 -0
  73. data/lib/ukiryu/models/search_paths.rb +39 -0
  74. data/lib/ukiryu/models/success_response.rb +85 -0
  75. data/lib/ukiryu/models/tool_definition.rb +145 -0
  76. data/lib/ukiryu/models/tool_metadata.rb +82 -0
  77. data/lib/ukiryu/models/validation_result.rb +80 -0
  78. data/lib/ukiryu/models/version_compatibility.rb +152 -0
  79. data/lib/ukiryu/models/version_detection.rb +39 -0
  80. data/lib/ukiryu/models.rb +23 -0
  81. data/lib/ukiryu/options/base.rb +95 -0
  82. data/lib/ukiryu/options_builder/formatter.rb +87 -0
  83. data/lib/ukiryu/options_builder/validator.rb +43 -0
  84. data/lib/ukiryu/options_builder.rb +311 -0
  85. data/lib/ukiryu/platform.rb +6 -6
  86. data/lib/ukiryu/registry.rb +143 -183
  87. data/lib/ukiryu/response/base.rb +217 -0
  88. data/lib/ukiryu/runtime.rb +179 -0
  89. data/lib/ukiryu/schema_validator.rb +8 -10
  90. data/lib/ukiryu/shell/bash.rb +3 -3
  91. data/lib/ukiryu/shell/cmd.rb +4 -4
  92. data/lib/ukiryu/shell/fish.rb +1 -1
  93. data/lib/ukiryu/shell/powershell.rb +3 -3
  94. data/lib/ukiryu/shell/sh.rb +1 -1
  95. data/lib/ukiryu/shell/zsh.rb +1 -1
  96. data/lib/ukiryu/shell.rb +146 -39
  97. data/lib/ukiryu/thor_ext.rb +208 -0
  98. data/lib/ukiryu/tool.rb +649 -258
  99. data/lib/ukiryu/tool_index.rb +224 -0
  100. data/lib/ukiryu/tools/base.rb +381 -0
  101. data/lib/ukiryu/tools/class_generator.rb +132 -0
  102. data/lib/ukiryu/tools/executable_finder.rb +29 -0
  103. data/lib/ukiryu/tools/generator.rb +154 -0
  104. data/lib/ukiryu/tools.rb +109 -0
  105. data/lib/ukiryu/type.rb +28 -43
  106. data/lib/ukiryu/validation/constraints.rb +281 -0
  107. data/lib/ukiryu/validation/validator.rb +188 -0
  108. data/lib/ukiryu/validation.rb +21 -0
  109. data/lib/ukiryu/version.rb +1 -1
  110. data/lib/ukiryu/version_detector.rb +51 -0
  111. data/lib/ukiryu.rb +31 -15
  112. data/ukiryu-proposal.md +2952 -0
  113. data/ukiryu.gemspec +18 -14
  114. metadata +137 -5
  115. data/.github/workflows/test.yml +0 -143
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ukiryu
4
+ module Validation
5
+ # Abstract base class for all constraints
6
+ #
7
+ # Constraints define validation rules that can be applied to values.
8
+ # Each constraint type encapsulates a specific validation logic.
9
+ #
10
+ # @abstract
11
+ class Constraint
12
+ # Validate a value against this constraint
13
+ #
14
+ # @param value [Object] the value to validate
15
+ # @param context [Hash] additional context for validation
16
+ # @return [void]
17
+ # @raise [ValidationError] if validation fails
18
+ def validate(value, context = {})
19
+ raise NotImplementedError, 'Subclasses must implement validate'
20
+ end
21
+
22
+ # Check if this constraint applies to the given context
23
+ #
24
+ # @param context [Hash] validation context
25
+ # @return [Boolean] true if constraint should be applied
26
+ def applies_to?(_context)
27
+ true
28
+ end
29
+ end
30
+
31
+ # Validates that a value is present (not nil or empty)
32
+ class RequiredConstraint < Constraint
33
+ # The name of the attribute being validated
34
+ attr_reader :attribute_name
35
+ # The minimum required cardinality
36
+ attr_reader :min
37
+
38
+ # @param attribute_name [String, Symbol] the attribute name
39
+ # @param min [Integer] minimum cardinality (default: 1)
40
+ def initialize(attribute_name, min: 1)
41
+ @attribute_name = attribute_name
42
+ @min = min
43
+ end
44
+
45
+ # @raise [ValidationError] if value is nil or empty when required
46
+ def validate(value, _context = {})
47
+ return if @min.zero?
48
+
49
+ is_empty = value.nil? || (value.respond_to?(:empty?) && value.empty?)
50
+ return unless is_empty
51
+
52
+ raise ValidationIssue.new(@attribute_name, :required,
53
+ "Attribute #{@attribute_name} is required but missing")
54
+ end
55
+ end
56
+
57
+ # Validates numeric or array length ranges
58
+ class RangeConstraint < Constraint
59
+ attr_reader :attribute_name, :min, :max
60
+
61
+ # @param attribute_name [String, Symbol] the attribute name
62
+ # @param min [Numeric] minimum value
63
+ # @param max [Numeric] maximum value
64
+ def initialize(attribute_name, min:, max:)
65
+ @attribute_name = attribute_name
66
+ @min = min
67
+ @max = max
68
+ end
69
+
70
+ # @raise [ValidationError] if value is outside range
71
+ def validate(value, _context = {})
72
+ return if value.nil?
73
+
74
+ check_value = value.is_a?(Array) ? value.size : value
75
+
76
+ return unless check_value < @min || check_value > @max
77
+
78
+ raise ValidationIssue.new(@attribute_name, :range,
79
+ "#{@attribute_name} must be between #{@min} and #{@max}, got #{check_value}")
80
+ end
81
+ end
82
+
83
+ # Validates that values match one of the allowed values
84
+ class EnumConstraint < Constraint
85
+ attr_reader :attribute_name, :allowed_values
86
+
87
+ # @param attribute_name [String, Symbol] the attribute name
88
+ # @param allowed_values [Array] the list of allowed values
89
+ def initialize(attribute_name, allowed_values:)
90
+ @attribute_name = attribute_name
91
+ @allowed_values = allowed_values
92
+ end
93
+
94
+ # @raise [ValidationError] if value is not in allowed values
95
+ def validate(value, _context = {})
96
+ return if value.nil?
97
+
98
+ if value.is_a?(Array)
99
+ invalid = value - @allowed_values
100
+ unless invalid.empty?
101
+ raise ValidationIssue.new(@attribute_name, :enum,
102
+ "#{@attribute_name} contains invalid values: #{invalid.join(', ')}. " \
103
+ "Valid values: #{@allowed_values.join(', ')}")
104
+ end
105
+ elsif !@allowed_values.include?(value)
106
+ raise ValidationIssue.new(@attribute_name, :enum,
107
+ "#{@attribute_name} must be one of #{@allowed_values.join(', ')}, got #{value}")
108
+ end
109
+ end
110
+ end
111
+
112
+ # Validates type constraints using the Type module
113
+ class TypeConstraint < Constraint
114
+ attr_reader :attribute_name, :type, :validation_options
115
+
116
+ # @param attribute_name [String, Symbol] the attribute name
117
+ # @param type [Symbol, Hash] the type definition
118
+ # @param validation_options [Hash] additional validation options
119
+ def initialize(attribute_name, type, validation_options: {})
120
+ @attribute_name = attribute_name
121
+ @type = type
122
+ @validation_options = validation_options
123
+ end
124
+
125
+ # @raise [ValidationError] if value doesn't match type
126
+ def validate(value, _context = {})
127
+ return if value.nil?
128
+
129
+ begin
130
+ Type.validate(value, @type, @validation_options)
131
+ rescue Ukiryu::ValidationError => e
132
+ raise ValidationIssue.new(@attribute_name, :type,
133
+ "#{@attribute_name}: #{e.message}")
134
+ end
135
+ end
136
+ end
137
+
138
+ # Validates cardinality constraints for variadic arguments
139
+ class CardinalityConstraint < Constraint
140
+ attr_reader :attribute_name, :min, :max
141
+
142
+ # @param attribute_name [String, Symbol] the attribute name
143
+ # @param min [Integer] minimum cardinality
144
+ # @param max [Integer, Float] maximum cardinality (Float::INFINITY for no limit)
145
+ def initialize(attribute_name, min:, max:)
146
+ @attribute_name = attribute_name
147
+ @min = min
148
+ @max = max
149
+ end
150
+
151
+ # @raise [ValidationError] if array size violates cardinality
152
+ def validate(value, _context = {})
153
+ return if value.nil? || !value.is_a?(Array)
154
+
155
+ return unless @max != Float::INFINITY && value.size > @max
156
+
157
+ raise ValidationIssue.new(@attribute_name, :cardinality,
158
+ "Too many values for #{@attribute_name}: got #{value.size}, max #{@max}")
159
+ end
160
+ end
161
+
162
+ # Validates that boolean flags are actually boolean
163
+ class BooleanFlagConstraint < Constraint
164
+ attr_reader :attribute_name
165
+
166
+ # @param attribute_name [String, Symbol] the attribute name
167
+ def initialize(attribute_name)
168
+ @attribute_name = attribute_name
169
+ end
170
+
171
+ # @raise [ValidationError] if flag is not boolean
172
+ def validate(value, _context = {})
173
+ return if value.nil?
174
+
175
+ return if [true, false].include?(value)
176
+
177
+ raise ValidationIssue.new(@attribute_name, :boolean,
178
+ "Flag #{@attribute_name} must be boolean, got #{value.class}: #{value}")
179
+ end
180
+ end
181
+
182
+ # Validates dependency constraints between options
183
+ class DependencyConstraint < Constraint
184
+ attr_reader :option_name, :requires, :conflicts, :implies
185
+
186
+ # @param option_name [String, Symbol] the option name
187
+ # @param requires [Array] options that must be present
188
+ # @param conflicts [Array] options that cannot be present
189
+ # @param implies [Hash] options that imply certain values
190
+ def initialize(option_name, requires: nil, conflicts: nil, implies: nil)
191
+ @option_name = option_name
192
+ @requires = requires
193
+ @conflicts = conflicts
194
+ @implies = implies
195
+ end
196
+
197
+ # @param value [Object] the value of the dependent option
198
+ # @param context [Hash] must contain :options_accessor to get other values
199
+ # @raise [ValidationError] if dependency constraints are violated
200
+ def validate(_value, context = {})
201
+ accessor = context[:options_accessor]
202
+ raise ArgumentError, 'Dependency validation requires :options_accessor' unless accessor
203
+
204
+ validate_requires(accessor)
205
+ validate_conflicts(accessor)
206
+ validate_implies(accessor)
207
+ end
208
+
209
+ private
210
+
211
+ def validate_requires(accessor)
212
+ return unless @requires
213
+
214
+ @requires.each do |required_opt|
215
+ required_value = accessor.call(required_opt)
216
+ if required_value.nil? || (required_value.is_a?(Array) && required_value.empty?)
217
+ raise ValidationIssue.new(@option_name, :dependency,
218
+ "Option #{@option_name} requires #{required_opt} to be set")
219
+ end
220
+ end
221
+ end
222
+
223
+ def validate_conflicts(accessor)
224
+ return unless @conflicts
225
+
226
+ @conflicts.each do |conflict_opt|
227
+ conflict_value = accessor.call(conflict_opt)
228
+ if conflict_value && !conflict_value.nil? && !(conflict_value.is_a?(Array) && conflict_value.empty?)
229
+ raise ValidationIssue.new(@option_name, :dependency,
230
+ "Option #{@option_name} conflicts with #{conflict_opt}")
231
+ end
232
+ end
233
+ end
234
+
235
+ def validate_implies(accessor)
236
+ return unless @implies
237
+
238
+ @implies.each do |implies_opt, implies_def|
239
+ current_value = accessor.call(implies_opt)
240
+ expected_value = implies_def[:value]
241
+ should_be_present = implies_def[:present]
242
+
243
+ if should_be_present && (current_value.nil? || (current_value.is_a?(Array) && current_value.empty?))
244
+ raise ValidationIssue.new(@option_name, :dependency,
245
+ "Option #{@option_name} implies #{implies_opt} should be set")
246
+ elsif !should_be_present && !current_value.nil?
247
+ raise ValidationIssue.new(@option_name, :dependency,
248
+ "Option #{@option_name} implies #{implies_opt} should not be set")
249
+ elsif expected_value && current_value != expected_value
250
+ raise ValidationIssue.new(@option_name, :dependency,
251
+ "Option #{@option_name} implies #{implies_opt} should be #{expected_value}, got #{current_value}")
252
+ end
253
+ end
254
+ end
255
+ end
256
+
257
+ # Validation issue represents a single validation problem
258
+ #
259
+ # This is a proper error object, not just a string.
260
+ class ValidationIssue < StandardError
261
+ # The attribute name that failed validation
262
+ attr_reader :attribute_name
263
+
264
+ # The type of validation that failed
265
+ attr_reader :validation_type
266
+
267
+ # Human-readable error message
268
+ attr_reader :message
269
+
270
+ # @param attribute_name [String, Symbol] the attribute that failed
271
+ # @param validation_type [Symbol] the type of validation
272
+ # @param message [String] human-readable error message
273
+ def initialize(attribute_name, validation_type, message)
274
+ @attribute_name = attribute_name
275
+ @validation_type = validation_type
276
+ @message = message
277
+ super(message)
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'constraints'
4
+
5
+ module Ukiryu
6
+ module Validation
7
+ # Validates option objects using constraint-based validation
8
+ #
9
+ # The Validator class applies a collection of constraints to an options
10
+ # object, ensuring all validation rules are satisfied before execution.
11
+ #
12
+ # This is a proper OOP validator that:
13
+ # - Uses constraint objects (not procedural code)
14
+ # - Returns validation result objects (not string arrays)
15
+ # - Provides clear separation of concerns
16
+ class Validator
17
+ # The options object being validated
18
+ attr_reader :options
19
+
20
+ # The command definition containing validation rules
21
+ attr_reader :command_def
22
+
23
+ # Collection of constraints to apply
24
+ attr_reader :constraints
25
+
26
+ # @param options [Object] the options object to validate
27
+ # @param command_def [Hash] the command definition
28
+ def initialize(options, command_def)
29
+ @options = options
30
+ @command_def = command_def
31
+ @constraints = []
32
+ build_constraints
33
+ end
34
+
35
+ # Perform validation
36
+ #
37
+ # @return [Boolean] true if validation passes
38
+ # @raise [ValidationError] if validation fails
39
+ def validate!
40
+ @constraints.each do |constraint|
41
+ constraint.validate(get_constraint_value(constraint), constraint_context)
42
+ end
43
+ true
44
+ rescue Validation::ValidationIssue => e
45
+ raise Ukiryu::ValidationError, e.message
46
+ end
47
+
48
+ # Check if validation would pass without raising errors
49
+ #
50
+ # @return [Boolean] true if valid, false otherwise
51
+ def valid?
52
+ validate!
53
+ true
54
+ rescue Ukiryu::ValidationError
55
+ false
56
+ end
57
+
58
+ # Get all validation errors
59
+ #
60
+ # @return [Array<String>] list of error messages
61
+ def errors
62
+ errors_list = []
63
+ @constraints.each do |constraint|
64
+ constraint.validate(get_constraint_value(constraint), constraint_context)
65
+ rescue Validation::ValidationIssue => e
66
+ errors_list << e.message
67
+ end
68
+ errors_list
69
+ end
70
+
71
+ private
72
+
73
+ # Build constraint objects from command definition
74
+ def build_constraints
75
+ build_argument_constraints
76
+ build_option_constraints(@command_def[:options])
77
+ build_option_constraints(@command_def[:post_options])
78
+ build_flag_constraints(@command_def[:flags])
79
+ build_dependency_constraints
80
+ end
81
+
82
+ # Build constraints for arguments
83
+ def build_argument_constraints
84
+ return unless @command_def[:arguments]
85
+
86
+ @command_def[:arguments].each do |arg_def|
87
+ attr_name = arg_def[:name]
88
+ min = arg_def[:min] || (arg_def[:variadic] ? 1 : 1)
89
+
90
+ # Required constraint
91
+ @constraints << RequiredConstraint.new(attr_name, min: min)
92
+
93
+ # Type constraint
94
+ @constraints << build_type_constraint(attr_name, arg_def) if arg_def[:type]
95
+
96
+ # Cardinality constraint for variadic arguments
97
+ next unless arg_def[:variadic] && arg_def[:max]
98
+
99
+ @constraints << CardinalityConstraint.new(attr_name,
100
+ min: min,
101
+ max: arg_def[:max])
102
+ end
103
+ end
104
+
105
+ # Build constraints for options
106
+ def build_option_constraints(options)
107
+ return unless options
108
+
109
+ options.each do |opt_def|
110
+ attr_name = opt_def[:name]
111
+
112
+ # Type constraint
113
+ @constraints << build_type_constraint(attr_name, opt_def) if opt_def[:type]
114
+
115
+ # Range constraint
116
+ if opt_def[:range]
117
+ min, max = opt_def[:range]
118
+ @constraints << RangeConstraint.new(attr_name, min: min, max: max)
119
+ end
120
+
121
+ # Enum constraint
122
+ @constraints << EnumConstraint.new(attr_name, allowed_values: opt_def[:values]) if opt_def[:values]
123
+ end
124
+ end
125
+
126
+ # Build constraints for flags
127
+ def build_flag_constraints(flags)
128
+ return unless flags
129
+
130
+ flags.each do |flag_def|
131
+ @constraints << BooleanFlagConstraint.new(flag_def[:name])
132
+ end
133
+ end
134
+
135
+ # Build dependency constraints
136
+ def build_dependency_constraints
137
+ dependencies = @command_def[:dependencies] || []
138
+ dependencies.each do |dep|
139
+ @constraints << DependencyConstraint.new(
140
+ dep[:option],
141
+ requires: dep[:requires],
142
+ conflicts: dep[:conflicts],
143
+ implies: dep[:implies]
144
+ )
145
+ end
146
+ end
147
+
148
+ # Build a type constraint from a definition
149
+ def build_type_constraint(attr_name, defn)
150
+ validation_opts = {}
151
+ validation_opts[:require_existing] = defn[:must_exist] if defn.key?(:must_exist)
152
+ validation_opts[:allow_empty] = defn[:allow_empty] if defn.key?(:allow_empty)
153
+ validation_opts[:pattern] = defn[:pattern] if defn[:pattern]
154
+ validation_opts[:range] = defn[:range] if defn[:range]
155
+ validation_opts[:min] = defn[:min] if defn[:min]
156
+ validation_opts[:max] = defn[:max] if defn[:max]
157
+ validation_opts[:values] = defn[:values] if defn[:values]
158
+
159
+ type = defn[:type]
160
+ validation_opts[:of] = type[:of] if type.is_a?(Hash) && type[:name] == :array && type[:of]
161
+ validation_opts[:keys] = type[:keys] if type.is_a?(Hash) && type[:name] == :hash && type[:keys]
162
+
163
+ TypeConstraint.new(attr_name, type, validation_options: validation_opts)
164
+ end
165
+
166
+ # Get the value for a constraint
167
+ #
168
+ # For attribute constraints, get the instance variable value.
169
+ # For dependency constraints, get the option's current value.
170
+ def get_constraint_value(constraint)
171
+ case constraint
172
+ when RequiredConstraint, TypeConstraint, RangeConstraint, EnumConstraint,
173
+ BooleanFlagConstraint, CardinalityConstraint
174
+ @options.instance_variable_get("@#{constraint.attribute_name}")
175
+ when DependencyConstraint
176
+ @options.instance_variable_get("@#{constraint.option_name}")
177
+ end
178
+ end
179
+
180
+ # Get context for constraint validation
181
+ def constraint_context
182
+ {
183
+ options_accessor: ->(attr_name) { @options.instance_variable_get("@#{attr_name}") }
184
+ }
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'validation/constraints'
4
+ require_relative 'validation/validator'
5
+
6
+ module Ukiryu
7
+ # Validation module for constraint-based option validation
8
+ #
9
+ # This module provides an OOP approach to validation using:
10
+ # - Constraint objects (not procedural code)
11
+ # - Validator classes that apply constraints
12
+ # - Proper error objects (not just strings)
13
+ #
14
+ # @example Validating options
15
+ # validator = Validation::Validator.new(options, command_def)
16
+ # validator.validate! # Raises ValidationError if invalid
17
+ # validator.valid? # Returns true/false
18
+ # validator.errors # Returns array of error messages
19
+ module Validation
20
+ end
21
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ukiryu
4
- VERSION = "0.1.0"
4
+ VERSION = '0.1.1'
5
5
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'executor'
4
+
5
+ module Ukiryu
6
+ # Version detector for external CLI tools
7
+ #
8
+ # This module provides centralized version detection logic with:
9
+ # - Configurable version command patterns
10
+ # - Regex pattern matching for version strings
11
+ # - Proper shell handling for command execution
12
+ #
13
+ # @example Detecting version
14
+ # version = VersionDetector.detect(
15
+ # executable: '/usr/bin/ffmpeg',
16
+ # command: '-version',
17
+ # pattern: /version (\d+\.\d+)/,
18
+ # shell: :bash
19
+ # )
20
+ module VersionDetector
21
+ class << self
22
+ # Detect the version of an external tool
23
+ #
24
+ # @param executable [String] the executable path
25
+ # @param command [String, Array<String>] the version command (default: '--version')
26
+ # @param pattern [Regexp] the regex pattern to extract version
27
+ # @param shell [Symbol] the shell to use for execution
28
+ # @return [String, nil] the detected version or nil if not found
29
+ def detect(executable:, command: '--version', pattern: /(\d+\.\d+)/, shell: nil)
30
+ # Return nil if executable is not found
31
+ return nil if executable.nil? || executable.empty?
32
+
33
+ shell ||= Shell.detect
34
+
35
+ # Normalize command to array
36
+ command_args = command.is_a?(Array) ? command : [command]
37
+
38
+ result = Executor.execute(executable, command_args, shell: shell)
39
+
40
+ return nil unless result.success?
41
+
42
+ # Sanitize strings to handle invalid UTF-8 sequences
43
+ stdout = result.stdout.scrub
44
+ stderr = result.stderr.scrub
45
+
46
+ match = stdout.match(pattern) || stderr.match(pattern)
47
+ match[1] if match
48
+ end
49
+ end
50
+ end
51
+ end
data/lib/ukiryu.rb CHANGED
@@ -1,28 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "ukiryu/version"
4
- require_relative "ukiryu/errors"
3
+ require_relative 'ukiryu/version'
4
+ require_relative 'ukiryu/errors'
5
5
 
6
6
  # Core modules
7
- require_relative "ukiryu/platform"
8
- require_relative "ukiryu/shell"
9
- require_relative "ukiryu/type"
10
- require_relative "ukiryu/executor"
11
- require_relative "ukiryu/registry"
12
- require_relative "ukiryu/tool"
13
- require_relative "ukiryu/schema_validator"
7
+ require_relative 'ukiryu/platform'
8
+ require_relative 'ukiryu/shell'
9
+ require_relative 'ukiryu/runtime'
10
+ require_relative 'ukiryu/execution_context'
11
+ require_relative 'ukiryu/type'
12
+ require_relative 'ukiryu/executor'
13
+ require_relative 'ukiryu/registry'
14
+ require_relative 'ukiryu/tool'
15
+ require_relative 'ukiryu/options_builder'
16
+ require_relative 'ukiryu/schema_validator'
17
+ require_relative 'ukiryu/io'
18
+
19
+ # Models - OOP representation of YAML profiles
20
+ require_relative 'ukiryu/models'
21
+
22
+ # New OOP modules
23
+ require_relative 'ukiryu/tools'
24
+ require_relative 'ukiryu/options/base'
25
+ require_relative 'ukiryu/response/base'
26
+ require_relative 'ukiryu/action/base'
27
+ require_relative 'ukiryu/validation'
28
+
29
+ # Extractors
30
+ require_relative 'ukiryu/extractors/extractor'
14
31
 
15
32
  # CLI (optional, only load if thor is available)
16
33
  begin
17
- require "thor"
18
- require_relative "ukiryu/cli"
34
+ require 'thor'
35
+ require_relative 'ukiryu/cli'
19
36
  rescue LoadError
20
37
  # Thor not available, CLI will not be available
21
38
  end
22
39
 
23
40
  module Ukiryu
24
- class Error < StandardError; end
25
-
26
41
  class << self
27
42
  # Configure global Ukiryu settings
28
43
  def configure
@@ -38,13 +53,14 @@ module Ukiryu
38
53
  def reset_configuration
39
54
  @configuration = nil
40
55
  Shell.reset
56
+ Runtime.instance.reset!
57
+ ExecutionContext.reset_current!
41
58
  end
42
59
  end
43
60
 
44
61
  # Configuration class for global settings
45
62
  class Configuration
46
- attr_accessor :default_shell
47
- attr_accessor :registry_path
63
+ attr_accessor :default_shell, :registry_path
48
64
 
49
65
  def initialize
50
66
  @default_shell = nil # Auto-detect by default