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,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ukiryu
|
|
4
|
+
module Tools
|
|
5
|
+
# Class generation utilities for tool-specific classes
|
|
6
|
+
#
|
|
7
|
+
# This module handles the dynamic generation of Options, Action, and Response
|
|
8
|
+
# classes for each tool command, keeping the Base class focused on execution.
|
|
9
|
+
module ClassGenerator
|
|
10
|
+
# Generate an options class for a command
|
|
11
|
+
#
|
|
12
|
+
# @param tool_class [Class] the tool class
|
|
13
|
+
# @param command_name [Symbol] the command name
|
|
14
|
+
# @param command_def [Models::CommandDefinition] the command definition
|
|
15
|
+
# @return [Class] the generated options class
|
|
16
|
+
def self.generate_options_class(tool_class, command_name, command_def)
|
|
17
|
+
require_relative '../options/base'
|
|
18
|
+
require_relative '../options_builder'
|
|
19
|
+
|
|
20
|
+
# Capture tool class in closure
|
|
21
|
+
tool_class_ref = tool_class
|
|
22
|
+
|
|
23
|
+
# Create class name
|
|
24
|
+
class_name = "#{command_name.to_s.capitalize}Options"
|
|
25
|
+
|
|
26
|
+
# Define the class in the tool's namespace
|
|
27
|
+
options_class = Class.new(::Ukiryu::Options::Base) do
|
|
28
|
+
# Store command definition
|
|
29
|
+
@command_def = command_def
|
|
30
|
+
@command_name = command_name
|
|
31
|
+
@tool_class = tool_class_ref
|
|
32
|
+
|
|
33
|
+
# Class methods
|
|
34
|
+
singleton_class.send(:define_method, :command_def) do
|
|
35
|
+
@command_def
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
singleton_class.send(:define_method, :command_name) do
|
|
39
|
+
@command_name
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
singleton_class.send(:define_method, :tool_class) do
|
|
43
|
+
@tool_class
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Define accessors using the OptionsBuilder
|
|
48
|
+
Ukiryu::OptionsBuilder.define_accessors(options_class, command_def)
|
|
49
|
+
Ukiryu::OptionsBuilder.define_to_shell_method(options_class, command_def)
|
|
50
|
+
Ukiryu::OptionsBuilder.define_validation_method(options_class, command_def)
|
|
51
|
+
|
|
52
|
+
# Define extra_args accessor for manual option injection
|
|
53
|
+
options_class.send(:attr_accessor, :extra_args)
|
|
54
|
+
|
|
55
|
+
# Define set() method for batch assignment
|
|
56
|
+
options_class.send(:define_method, :set) do |params|
|
|
57
|
+
params.each do |key, value|
|
|
58
|
+
setter = "#{key}="
|
|
59
|
+
send(setter, value) if respond_to?(setter)
|
|
60
|
+
end
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Define run() method that executes on the associated tool
|
|
65
|
+
options_class.send(:define_method, :run) do
|
|
66
|
+
# Validate options before execution
|
|
67
|
+
validate!
|
|
68
|
+
tool_instance = tool_class_ref.new
|
|
69
|
+
tool_instance.execute(command_name, self)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Const the class in the tool's namespace
|
|
73
|
+
tool_class_ref.const_set(class_name, options_class) unless tool_class_ref.const_defined?(class_name)
|
|
74
|
+
|
|
75
|
+
options_class
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Generate an action class for a command
|
|
79
|
+
#
|
|
80
|
+
# @param tool_class [Class] the tool class
|
|
81
|
+
# @param command_name [Symbol] the command name
|
|
82
|
+
# @param command_def [Models::CommandDefinition] the command definition
|
|
83
|
+
# @return [Class] the generated action class
|
|
84
|
+
def self.generate_action_class(tool_class, command_name, command_def)
|
|
85
|
+
require_relative '../action/base'
|
|
86
|
+
|
|
87
|
+
class_name = "#{command_name.to_s.capitalize}Action"
|
|
88
|
+
|
|
89
|
+
action_class = Class.new(::Ukiryu::Action::Base) do
|
|
90
|
+
@command_name = command_name
|
|
91
|
+
@command_def = command_def
|
|
92
|
+
@tool_class = tool_class
|
|
93
|
+
|
|
94
|
+
singleton_class.send(:define_method, :command_name) do
|
|
95
|
+
@command_name
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
singleton_class.send(:define_method, :command_def) do
|
|
99
|
+
@command_def
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
singleton_class.send(:define_method, :tool_class) do
|
|
103
|
+
@tool_class
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
tool_class.const_set(class_name, action_class) unless tool_class.const_defined?(class_name)
|
|
108
|
+
action_class
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Generate a response class for a command
|
|
112
|
+
#
|
|
113
|
+
# @param tool_class [Class] the tool class
|
|
114
|
+
# @param command_name [Symbol] the command name
|
|
115
|
+
# @param command_def [Models::CommandDefinition] the command definition
|
|
116
|
+
# @return [Class] the generated response class
|
|
117
|
+
def self.generate_response_class(tool_class, command_name, command_def)
|
|
118
|
+
require_relative '../response/base'
|
|
119
|
+
|
|
120
|
+
class_name = "#{command_name.to_s.capitalize}Response"
|
|
121
|
+
|
|
122
|
+
response_class = Class.new(::Ukiryu::Response::Base) do
|
|
123
|
+
@command_name = command_name
|
|
124
|
+
@command_def = command_def
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
tool_class.const_set(class_name, response_class) unless tool_class.const_defined?(class_name)
|
|
128
|
+
response_class
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ukiryu
|
|
4
|
+
module Tools
|
|
5
|
+
# Executable finder utilities for tool classes
|
|
6
|
+
#
|
|
7
|
+
# This module provides methods to find executables on the system
|
|
8
|
+
# using search paths and aliases from tool definitions.
|
|
9
|
+
module ExecutableFinder
|
|
10
|
+
# Find the executable for a tool
|
|
11
|
+
#
|
|
12
|
+
# @param tool_name [String] the tool name
|
|
13
|
+
# @param tool_definition [Models::ToolDefinition] the tool definition
|
|
14
|
+
# @return [String, nil] path to executable or nil
|
|
15
|
+
def self.find_executable(tool_name, tool_definition)
|
|
16
|
+
require_relative '../executable_locator'
|
|
17
|
+
|
|
18
|
+
platform = Ukiryu::Runtime.instance.platform
|
|
19
|
+
|
|
20
|
+
ExecutableLocator.find(
|
|
21
|
+
tool_name: tool_name,
|
|
22
|
+
aliases: tool_definition.aliases || [],
|
|
23
|
+
search_paths: tool_definition.search_paths,
|
|
24
|
+
platform: platform
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../models/tool_definition'
|
|
4
|
+
require_relative '../cache'
|
|
5
|
+
|
|
6
|
+
module Ukiryu
|
|
7
|
+
module Tools
|
|
8
|
+
# Generator module for dynamically creating tool-specific classes
|
|
9
|
+
#
|
|
10
|
+
# This module is responsible for:
|
|
11
|
+
# - Loading tool definitions from YAML as lutaml-model models
|
|
12
|
+
# - Generating tool classes (e.g., Ukiryu::Tools::Imagemagick)
|
|
13
|
+
# - Caching generated classes with bounded LRU cache
|
|
14
|
+
# - Providing constant autoloading via const_missing
|
|
15
|
+
#
|
|
16
|
+
module Generator
|
|
17
|
+
class << self
|
|
18
|
+
# Get the generated classes cache (bounded LRU cache)
|
|
19
|
+
#
|
|
20
|
+
# @return [Cache] the generated classes cache
|
|
21
|
+
def generated_classes_cache
|
|
22
|
+
@generated_classes_cache ||= Cache.new(max_size: 50, ttl: 3600)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Get or generate a tool class
|
|
26
|
+
#
|
|
27
|
+
# @param tool_name [Symbol, String] the tool name
|
|
28
|
+
# @return [Class] the tool class
|
|
29
|
+
def generate(tool_name)
|
|
30
|
+
tool_name = tool_name.to_sym
|
|
31
|
+
cached = generated_classes_cache[tool_name]
|
|
32
|
+
return cached if cached
|
|
33
|
+
|
|
34
|
+
# Load the tool definition as a lutaml-model
|
|
35
|
+
tool_definition = load_tool_definition(tool_name)
|
|
36
|
+
raise Ukiryu::ToolNotFoundError, "Tool not found: #{tool_name}" unless tool_definition
|
|
37
|
+
|
|
38
|
+
# Get the compatible platform profile
|
|
39
|
+
platform_profile = tool_definition.compatible_profile
|
|
40
|
+
|
|
41
|
+
# Generate the tool class
|
|
42
|
+
tool_class = generate_tool_class(tool_name, tool_definition, platform_profile)
|
|
43
|
+
|
|
44
|
+
generated_classes_cache[tool_name] = tool_class
|
|
45
|
+
tool_class
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Load a ToolDefinition model from the registry
|
|
49
|
+
#
|
|
50
|
+
# @param tool_name [Symbol] the tool name
|
|
51
|
+
# @return [Models::ToolDefinition, nil] the tool definition model
|
|
52
|
+
def load_tool_definition(tool_name)
|
|
53
|
+
require_relative '../registry'
|
|
54
|
+
|
|
55
|
+
# Load the YAML file content
|
|
56
|
+
yaml_content = Registry.load_tool_yaml(tool_name)
|
|
57
|
+
return nil unless yaml_content
|
|
58
|
+
|
|
59
|
+
# Use lutaml-model's from_yaml to parse
|
|
60
|
+
tool_definition = Models::ToolDefinition.from_yaml(yaml_content)
|
|
61
|
+
|
|
62
|
+
# Resolve profile inheritance (e.g., windows profiles inherit from unix)
|
|
63
|
+
tool_definition&.resolve_inheritance!
|
|
64
|
+
|
|
65
|
+
tool_definition
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Generate a tool class from a tool definition
|
|
69
|
+
#
|
|
70
|
+
# @param tool_name [Symbol] the tool name
|
|
71
|
+
# @param tool_definition [Models::ToolDefinition] the tool definition
|
|
72
|
+
# @param platform_profile [Models::PlatformProfile] the compatible platform profile
|
|
73
|
+
# @return [Class] the generated tool class
|
|
74
|
+
def generate_tool_class(tool_name, tool_definition, platform_profile)
|
|
75
|
+
Class.new(::Ukiryu::Tools::Base) do
|
|
76
|
+
@tool_name = tool_name
|
|
77
|
+
@tool_definition = tool_definition
|
|
78
|
+
@platform_profile = platform_profile
|
|
79
|
+
|
|
80
|
+
# Define class methods
|
|
81
|
+
singleton_class.send(:define_method, :tool_name) do
|
|
82
|
+
@tool_name
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
singleton_class.send(:define_method, :tool_definition) do
|
|
86
|
+
@tool_definition
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
singleton_class.send(:define_method, :platform_profile) do
|
|
90
|
+
@platform_profile
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Generate a tool class and const it in the Tools namespace
|
|
96
|
+
#
|
|
97
|
+
# @param tool_name [Symbol] the tool name
|
|
98
|
+
# @return [Class] the tool class
|
|
99
|
+
def generate_and_const_set(tool_name)
|
|
100
|
+
tool_class = generate(tool_name)
|
|
101
|
+
class_name = tool_name.to_s.capitalize
|
|
102
|
+
|
|
103
|
+
# Const the class in the Tools module
|
|
104
|
+
Ukiryu::Tools.const_set(class_name, tool_class) unless Ukiryu::Tools.const_defined?(class_name)
|
|
105
|
+
|
|
106
|
+
tool_class
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Clear the cache of generated classes
|
|
110
|
+
#
|
|
111
|
+
# Useful for testing or reloading profiles
|
|
112
|
+
def clear_cache
|
|
113
|
+
generated_classes_cache.clear
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Check if a tool class has been generated
|
|
117
|
+
#
|
|
118
|
+
# @param tool_name [Symbol] the tool name
|
|
119
|
+
# @return [Boolean] true if the class has been generated
|
|
120
|
+
def generated?(tool_name)
|
|
121
|
+
generated_classes_cache.key?(tool_name.to_sym)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Get all generated tool classes
|
|
125
|
+
#
|
|
126
|
+
# @return [Hash] map of tool name to class
|
|
127
|
+
def all_generated
|
|
128
|
+
result = {}
|
|
129
|
+
generated_classes_cache.each_key do |key|
|
|
130
|
+
result[key] = generated_classes_cache[key]
|
|
131
|
+
end
|
|
132
|
+
result
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Get a list of all available tool names
|
|
136
|
+
#
|
|
137
|
+
# @return [Array<Symbol>] list of tool names
|
|
138
|
+
def available_tools
|
|
139
|
+
require_relative '../registry'
|
|
140
|
+
|
|
141
|
+
registry_path = Registry.default_registry_path
|
|
142
|
+
return [] unless registry_path
|
|
143
|
+
|
|
144
|
+
tools_dir = File.join(registry_path, 'tools')
|
|
145
|
+
return [] unless Dir.exist?(tools_dir)
|
|
146
|
+
|
|
147
|
+
Dir.entries(tools_dir)
|
|
148
|
+
.reject { |e| e.start_with?('.') }
|
|
149
|
+
.map(&:to_sym)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
data/lib/ukiryu/tools.rb
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'tools/base'
|
|
4
|
+
require_relative 'tools/generator'
|
|
5
|
+
|
|
6
|
+
module Ukiryu
|
|
7
|
+
# Tools namespace for tool-specific classes
|
|
8
|
+
#
|
|
9
|
+
# This namespace provides lazy-autoloaded tool classes.
|
|
10
|
+
# When you reference Ukiryu::Tools::Imagemagick, it automatically
|
|
11
|
+
# generates the class if it doesn't exist.
|
|
12
|
+
#
|
|
13
|
+
# Platform aliases are also supported - e.g., Ping resolves to PingBsd
|
|
14
|
+
# on macOS or PingGnu on Linux.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# Ukiryu::Tools::Imagemagick.new.tap do |tool|
|
|
18
|
+
# options = tool.options_for(:convert)
|
|
19
|
+
# options.set(inputs: ["image.png"], resize: "50%")
|
|
20
|
+
# options.run
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# @example Platform alias
|
|
24
|
+
# # Automatically uses PingBsd on macOS, PingGnu on Linux
|
|
25
|
+
# Ukiryu::Tools::Ping.new.execute(:ping, host: 'localhost', count: 1)
|
|
26
|
+
module Tools
|
|
27
|
+
class << self
|
|
28
|
+
# Autoload tool classes via const_missing
|
|
29
|
+
#
|
|
30
|
+
# When you reference Ukiryu::Tools::Imagemagick, this method automatically
|
|
31
|
+
# generates the class if it doesn't exist.
|
|
32
|
+
#
|
|
33
|
+
# Platform aliases are resolved first - e.g., Ping resolves to PingBsd
|
|
34
|
+
# on macOS or PingGnu on Linux based on the current platform.
|
|
35
|
+
#
|
|
36
|
+
# @param name [String, Symbol] the constant name
|
|
37
|
+
# @return [Class] the generated tool class
|
|
38
|
+
def const_missing(name)
|
|
39
|
+
# Convert CamelCase constant name to snake_case tool name
|
|
40
|
+
# e.g., PingBsd -> ping_bsd, PingGnu -> ping_gnu, Ping -> ping
|
|
41
|
+
tool_name_str = name.to_s
|
|
42
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') # Add underscore before caps that follow lowercase
|
|
43
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2') # Add underscore between lowercase and uppercase
|
|
44
|
+
.downcase
|
|
45
|
+
|
|
46
|
+
tool_name = tool_name_str.to_sym
|
|
47
|
+
|
|
48
|
+
# First, check if it's a platform alias
|
|
49
|
+
# Look for tools that implement this alias for the current platform
|
|
50
|
+
platform_impl = find_platform_implementation(tool_name)
|
|
51
|
+
return Generator.generate_and_const_set(platform_impl) if platform_impl
|
|
52
|
+
|
|
53
|
+
# If not an alias, try to generate the tool directly
|
|
54
|
+
generated = Generator.generate_and_const_set(tool_name)
|
|
55
|
+
return generated if generated
|
|
56
|
+
|
|
57
|
+
# If nothing found, let the error propagate
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# Find a platform-specific implementation for a tool alias
|
|
64
|
+
#
|
|
65
|
+
# @param alias_name [Symbol] the alias to resolve
|
|
66
|
+
# @return [Symbol, nil] the platform-specific tool name
|
|
67
|
+
def find_platform_implementation(alias_name)
|
|
68
|
+
registry_path = Registry.default_registry_path
|
|
69
|
+
return nil unless registry_path && Dir.exist?(registry_path)
|
|
70
|
+
|
|
71
|
+
tools_dir = File.join(registry_path, 'tools')
|
|
72
|
+
return nil unless Dir.exist?(tools_dir)
|
|
73
|
+
|
|
74
|
+
current_platform = Platform.detect
|
|
75
|
+
|
|
76
|
+
# Search through all tool directories
|
|
77
|
+
Dir.entries(tools_dir).each do |tool_dir|
|
|
78
|
+
next if tool_dir.start_with?('.')
|
|
79
|
+
|
|
80
|
+
tool_path = File.join(tools_dir, tool_dir)
|
|
81
|
+
next unless File.directory?(tool_path)
|
|
82
|
+
|
|
83
|
+
# Look for YAML files in this directory
|
|
84
|
+
Dir.entries(tool_path).each do |yaml_file|
|
|
85
|
+
next unless yaml_file.end_with?('.yaml')
|
|
86
|
+
|
|
87
|
+
yaml_path = File.join(tool_path, yaml_file)
|
|
88
|
+
profile = YAML.load_file(yaml_path, symbolize_names: true)
|
|
89
|
+
|
|
90
|
+
# Check if this tool implements the alias and matches current platform
|
|
91
|
+
# Note: implements field is a string in YAML, convert to symbol for comparison
|
|
92
|
+
next unless profile[:implements]&.to_sym == alias_name
|
|
93
|
+
|
|
94
|
+
# Check platform compatibility
|
|
95
|
+
profiles = profile[:profiles] || []
|
|
96
|
+
compatible = profiles.any? do |p|
|
|
97
|
+
platforms = p[:platforms] || p[:platform]
|
|
98
|
+
platforms.nil? || platforms.map(&:to_sym).include?(current_platform)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
return tool_dir.to_sym if compatible
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/ukiryu/type.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'uri'
|
|
4
|
+
require 'time'
|
|
5
5
|
|
|
6
6
|
module Ukiryu
|
|
7
7
|
# Type validation and conversion
|
|
@@ -75,12 +75,10 @@ module Ukiryu
|
|
|
75
75
|
# Validate file type
|
|
76
76
|
def validate_file(value, options)
|
|
77
77
|
value = value.to_s
|
|
78
|
-
raise ValidationError,
|
|
78
|
+
raise ValidationError, 'File path cannot be empty' if value.empty?
|
|
79
79
|
|
|
80
80
|
# Check if file exists (only if require_existing is true)
|
|
81
|
-
if options[:require_existing] && !File.exist?(value)
|
|
82
|
-
raise ValidationError, "File not found: #{value}"
|
|
83
|
-
end
|
|
81
|
+
raise ValidationError, "File not found: #{value}" if options[:require_existing] && !File.exist?(value)
|
|
84
82
|
|
|
85
83
|
value
|
|
86
84
|
end
|
|
@@ -88,11 +86,9 @@ module Ukiryu
|
|
|
88
86
|
# Validate string type
|
|
89
87
|
def validate_string(value, options)
|
|
90
88
|
value = value.to_s
|
|
91
|
-
raise ValidationError,
|
|
89
|
+
raise ValidationError, 'String cannot be empty' if value.empty? && !options[:allow_empty]
|
|
92
90
|
|
|
93
|
-
if options[:pattern] &&
|
|
94
|
-
raise ValidationError, "String does not match required pattern: #{options[:pattern]}"
|
|
95
|
-
end
|
|
91
|
+
raise ValidationError, "String does not match required pattern: #{options[:pattern]}" if options[:pattern] && value !~ options[:pattern]
|
|
96
92
|
|
|
97
93
|
value
|
|
98
94
|
end
|
|
@@ -109,17 +105,17 @@ module Ukiryu
|
|
|
109
105
|
|
|
110
106
|
if options[:range]
|
|
111
107
|
min, max = options[:range]
|
|
112
|
-
if integer < min || integer > max
|
|
113
|
-
raise ValidationError, "Integer #{integer} out of range [#{min}, #{max}]"
|
|
114
|
-
end
|
|
108
|
+
raise ValidationError, "Integer #{integer} out of range [#{min}, #{max}]" if integer < min || integer > max
|
|
115
109
|
end
|
|
116
110
|
|
|
117
111
|
if options[:min] && integer < options[:min]
|
|
118
|
-
raise ValidationError,
|
|
112
|
+
raise ValidationError,
|
|
113
|
+
"Integer #{integer} below minimum #{options[:min]}"
|
|
119
114
|
end
|
|
120
115
|
|
|
121
116
|
if options[:max] && integer > options[:max]
|
|
122
|
-
raise ValidationError,
|
|
117
|
+
raise ValidationError,
|
|
118
|
+
"Integer #{integer} above maximum #{options[:max]}"
|
|
123
119
|
end
|
|
124
120
|
|
|
125
121
|
integer
|
|
@@ -137,9 +133,7 @@ module Ukiryu
|
|
|
137
133
|
|
|
138
134
|
if options[:range]
|
|
139
135
|
min, max = options[:range]
|
|
140
|
-
if float < min || float > max
|
|
141
|
-
raise ValidationError, "Float #{float} out of range [#{min}, #{max}]"
|
|
142
|
-
end
|
|
136
|
+
raise ValidationError, "Float #{float} out of range [#{min}, #{max}]" if float < min || float > max
|
|
143
137
|
end
|
|
144
138
|
|
|
145
139
|
float
|
|
@@ -152,9 +146,7 @@ module Ukiryu
|
|
|
152
146
|
if options[:values]
|
|
153
147
|
# Convert values to symbols for comparison (handle both string and symbol values)
|
|
154
148
|
valid_values = options[:values].map { |v| v.is_a?(String) ? v.to_sym : v }
|
|
155
|
-
unless valid_values.include?(value)
|
|
156
|
-
raise ValidationError, "Invalid symbol: #{value.inspect}. Valid values: #{options[:values].inspect}"
|
|
157
|
-
end
|
|
149
|
+
raise ValidationError, "Invalid symbol: #{value.inspect}. Valid values: #{options[:values].inspect}" unless valid_values.include?(value)
|
|
158
150
|
end
|
|
159
151
|
|
|
160
152
|
value
|
|
@@ -166,9 +158,9 @@ module Ukiryu
|
|
|
166
158
|
case value
|
|
167
159
|
when TrueClass, FalseClass
|
|
168
160
|
value
|
|
169
|
-
when
|
|
161
|
+
when 'true', '1', 'yes', 'on'
|
|
170
162
|
true
|
|
171
|
-
when
|
|
163
|
+
when 'false', '0', 'no', 'off', ''
|
|
172
164
|
false
|
|
173
165
|
else
|
|
174
166
|
raise ValidationError, "Invalid boolean: #{value.inspect}"
|
|
@@ -176,13 +168,14 @@ module Ukiryu
|
|
|
176
168
|
end
|
|
177
169
|
|
|
178
170
|
# Validate URI type
|
|
179
|
-
def validate_uri(value,
|
|
171
|
+
def validate_uri(value, _options)
|
|
180
172
|
value = value.to_s
|
|
181
|
-
raise ValidationError,
|
|
173
|
+
raise ValidationError, 'URI cannot be empty' if value.empty?
|
|
182
174
|
|
|
183
175
|
begin
|
|
184
176
|
uri = URI.parse(value)
|
|
185
177
|
raise ValidationError, "Invalid URI: #{value}" unless uri.is_a?(URI::Generic)
|
|
178
|
+
|
|
186
179
|
uri.to_s
|
|
187
180
|
rescue URI::InvalidURIError => e
|
|
188
181
|
raise ValidationError, "Invalid URI: #{value} - #{e.message}"
|
|
@@ -190,7 +183,7 @@ module Ukiryu
|
|
|
190
183
|
end
|
|
191
184
|
|
|
192
185
|
# Validate datetime type
|
|
193
|
-
def validate_datetime(value,
|
|
186
|
+
def validate_datetime(value, _options)
|
|
194
187
|
if value.is_a?(Time) || value.is_a?(DateTime)
|
|
195
188
|
value
|
|
196
189
|
else
|
|
@@ -204,14 +197,13 @@ module Ukiryu
|
|
|
204
197
|
|
|
205
198
|
# Validate hash type
|
|
206
199
|
def validate_hash(value, options)
|
|
207
|
-
unless value.is_a?(Hash)
|
|
208
|
-
raise ValidationError, "Hash expected, got #{value.class}: #{value.inspect}"
|
|
209
|
-
end
|
|
200
|
+
raise ValidationError, "Hash expected, got #{value.class}: #{value.inspect}" unless value.is_a?(Hash)
|
|
210
201
|
|
|
211
202
|
if options[:keys]
|
|
212
203
|
unknown_keys = value.keys - options[:keys]
|
|
213
204
|
if unknown_keys.any?
|
|
214
|
-
raise ValidationError,
|
|
205
|
+
raise ValidationError,
|
|
206
|
+
"Unknown hash keys: #{unknown_keys.inspect}. Valid keys: #{options[:keys].inspect}"
|
|
215
207
|
end
|
|
216
208
|
end
|
|
217
209
|
|
|
@@ -222,30 +214,23 @@ module Ukiryu
|
|
|
222
214
|
def validate_array(value, options)
|
|
223
215
|
array = value.is_a?(Array) ? value : [value]
|
|
224
216
|
|
|
225
|
-
if options[:min] && array.size < options[:min]
|
|
226
|
-
raise ValidationError, "Array has #{array.size} elements, minimum is #{options[:min]}"
|
|
227
|
-
end
|
|
217
|
+
raise ValidationError, "Array has #{array.size} elements, minimum is #{options[:min]}" if options[:min] && array.size < options[:min]
|
|
228
218
|
|
|
229
|
-
if options[:max] && array.size > options[:max]
|
|
230
|
-
raise ValidationError, "Array has #{array.size} elements, maximum is #{options[:max]}"
|
|
231
|
-
end
|
|
219
|
+
raise ValidationError, "Array has #{array.size} elements, maximum is #{options[:max]}" if options[:max] && array.size > options[:max]
|
|
232
220
|
|
|
233
221
|
if options[:size]
|
|
234
222
|
if options[:size].is_a?(Integer)
|
|
235
223
|
if array.size != options[:size]
|
|
236
|
-
raise ValidationError,
|
|
224
|
+
raise ValidationError,
|
|
225
|
+
"Array has #{array.size} elements, expected #{options[:size]}"
|
|
237
226
|
end
|
|
238
227
|
elsif options[:size].is_a?(Array)
|
|
239
|
-
unless options[:size].include?(array.size)
|
|
240
|
-
raise ValidationError, "Array has #{array.size} elements, expected one of: #{options[:size].inspect}"
|
|
241
|
-
end
|
|
228
|
+
raise ValidationError, "Array has #{array.size} elements, expected one of: #{options[:size].inspect}" unless options[:size].include?(array.size)
|
|
242
229
|
end
|
|
243
230
|
end
|
|
244
231
|
|
|
245
232
|
# Validate element type if specified
|
|
246
|
-
if options[:of]
|
|
247
|
-
array = array.map { |v| validate(v, options[:of], options) }
|
|
248
|
-
end
|
|
233
|
+
array = array.map { |v| validate(v, options[:of], options) } if options[:of]
|
|
249
234
|
|
|
250
235
|
array
|
|
251
236
|
end
|