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.
- checksums.yaml +4 -4
- data/.github/workflows/docs.yml +63 -0
- data/.github/workflows/links.yml +99 -0
- data/.github/workflows/rake.yml +19 -0
- data/.github/workflows/release.yml +27 -0
- data/.gitignore +18 -4
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +213 -0
- data/Gemfile +12 -8
- data/README.adoc +613 -0
- data/Rakefile +2 -2
- data/docs/assets/logo.svg +1 -0
- data/exe/ukiryu +11 -0
- data/lib/ukiryu/action/base.rb +77 -0
- data/lib/ukiryu/cache.rb +199 -0
- data/lib/ukiryu/cli.rb +133 -307
- data/lib/ukiryu/cli_commands/base_command.rb +155 -0
- data/lib/ukiryu/cli_commands/commands_command.rb +120 -0
- data/lib/ukiryu/cli_commands/commands_command.rb.fixed +40 -0
- data/lib/ukiryu/cli_commands/config_command.rb +249 -0
- data/lib/ukiryu/cli_commands/describe_command.rb +326 -0
- data/lib/ukiryu/cli_commands/describe_command.rb.fixed +254 -0
- data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +180 -0
- data/lib/ukiryu/cli_commands/extract_command.rb +84 -0
- data/lib/ukiryu/cli_commands/info_command.rb +156 -0
- data/lib/ukiryu/cli_commands/list_command.rb +70 -0
- data/lib/ukiryu/cli_commands/opts_command.rb +106 -0
- data/lib/ukiryu/cli_commands/opts_command.rb.fixed +105 -0
- data/lib/ukiryu/cli_commands/response_formatter.rb +240 -0
- data/lib/ukiryu/cli_commands/run_command.rb +375 -0
- data/lib/ukiryu/cli_commands/run_file_command.rb +215 -0
- data/lib/ukiryu/cli_commands/system_command.rb +90 -0
- data/lib/ukiryu/cli_commands/validate_command.rb +87 -0
- data/lib/ukiryu/cli_commands/version_command.rb +16 -0
- data/lib/ukiryu/cli_commands/which_command.rb +166 -0
- data/lib/ukiryu/command_builder.rb +205 -0
- data/lib/ukiryu/config/env_provider.rb +64 -0
- data/lib/ukiryu/config/env_schema.rb +63 -0
- data/lib/ukiryu/config/override_resolver.rb +68 -0
- data/lib/ukiryu/config/type_converter.rb +59 -0
- data/lib/ukiryu/config.rb +249 -0
- data/lib/ukiryu/errors.rb +3 -0
- data/lib/ukiryu/executable_locator.rb +114 -0
- data/lib/ukiryu/execution/command_info.rb +64 -0
- data/lib/ukiryu/execution/metadata.rb +97 -0
- data/lib/ukiryu/execution/output.rb +144 -0
- data/lib/ukiryu/execution/result.rb +194 -0
- data/lib/ukiryu/execution.rb +15 -0
- data/lib/ukiryu/execution_context.rb +251 -0
- data/lib/ukiryu/executor.rb +76 -493
- data/lib/ukiryu/extractors/base_extractor.rb +63 -0
- data/lib/ukiryu/extractors/extractor.rb +150 -0
- data/lib/ukiryu/extractors/help_parser.rb +188 -0
- data/lib/ukiryu/extractors/native_extractor.rb +47 -0
- data/lib/ukiryu/io.rb +196 -0
- data/lib/ukiryu/logger.rb +544 -0
- data/lib/ukiryu/models/argument.rb +28 -0
- data/lib/ukiryu/models/argument_definition.rb +119 -0
- data/lib/ukiryu/models/arguments.rb +113 -0
- data/lib/ukiryu/models/command_definition.rb +176 -0
- data/lib/ukiryu/models/command_info.rb +37 -0
- data/lib/ukiryu/models/components.rb +107 -0
- data/lib/ukiryu/models/env_var_definition.rb +30 -0
- data/lib/ukiryu/models/error_response.rb +41 -0
- data/lib/ukiryu/models/execution_metadata.rb +31 -0
- data/lib/ukiryu/models/execution_report.rb +236 -0
- data/lib/ukiryu/models/exit_codes.rb +74 -0
- data/lib/ukiryu/models/flag_definition.rb +67 -0
- data/lib/ukiryu/models/option_definition.rb +102 -0
- data/lib/ukiryu/models/output_info.rb +25 -0
- data/lib/ukiryu/models/platform_profile.rb +153 -0
- data/lib/ukiryu/models/routing.rb +211 -0
- data/lib/ukiryu/models/search_paths.rb +39 -0
- data/lib/ukiryu/models/success_response.rb +85 -0
- data/lib/ukiryu/models/tool_definition.rb +145 -0
- data/lib/ukiryu/models/tool_metadata.rb +82 -0
- data/lib/ukiryu/models/validation_result.rb +80 -0
- data/lib/ukiryu/models/version_compatibility.rb +152 -0
- data/lib/ukiryu/models/version_detection.rb +39 -0
- data/lib/ukiryu/models.rb +23 -0
- data/lib/ukiryu/options/base.rb +95 -0
- data/lib/ukiryu/options_builder/formatter.rb +87 -0
- data/lib/ukiryu/options_builder/validator.rb +43 -0
- data/lib/ukiryu/options_builder.rb +311 -0
- data/lib/ukiryu/platform.rb +6 -6
- data/lib/ukiryu/registry.rb +143 -183
- data/lib/ukiryu/response/base.rb +217 -0
- data/lib/ukiryu/runtime.rb +179 -0
- data/lib/ukiryu/schema_validator.rb +8 -10
- data/lib/ukiryu/shell/bash.rb +3 -3
- data/lib/ukiryu/shell/cmd.rb +4 -4
- data/lib/ukiryu/shell/fish.rb +1 -1
- data/lib/ukiryu/shell/powershell.rb +3 -3
- data/lib/ukiryu/shell/sh.rb +1 -1
- data/lib/ukiryu/shell/zsh.rb +1 -1
- data/lib/ukiryu/shell.rb +146 -39
- data/lib/ukiryu/thor_ext.rb +208 -0
- data/lib/ukiryu/tool.rb +649 -258
- data/lib/ukiryu/tool_index.rb +224 -0
- data/lib/ukiryu/tools/base.rb +381 -0
- data/lib/ukiryu/tools/class_generator.rb +132 -0
- data/lib/ukiryu/tools/executable_finder.rb +29 -0
- data/lib/ukiryu/tools/generator.rb +154 -0
- data/lib/ukiryu/tools.rb +109 -0
- data/lib/ukiryu/type.rb +28 -43
- data/lib/ukiryu/validation/constraints.rb +281 -0
- data/lib/ukiryu/validation/validator.rb +188 -0
- data/lib/ukiryu/validation.rb +21 -0
- data/lib/ukiryu/version.rb +1 -1
- data/lib/ukiryu/version_detector.rb +51 -0
- data/lib/ukiryu.rb +31 -15
- data/ukiryu-proposal.md +2952 -0
- data/ukiryu.gemspec +18 -14
- metadata +137 -5
- 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
|
data/lib/ukiryu/version.rb
CHANGED
|
@@ -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
|
|
4
|
-
require_relative
|
|
3
|
+
require_relative 'ukiryu/version'
|
|
4
|
+
require_relative 'ukiryu/errors'
|
|
5
5
|
|
|
6
6
|
# Core modules
|
|
7
|
-
require_relative
|
|
8
|
-
require_relative
|
|
9
|
-
require_relative
|
|
10
|
-
require_relative
|
|
11
|
-
require_relative
|
|
12
|
-
require_relative
|
|
13
|
-
require_relative
|
|
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
|
|
18
|
-
require_relative
|
|
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
|