Class: Ace::Support::Nav::Molecules::ProtocolScanner
- Inherits:
-
Object
- Object
- Ace::Support::Nav::Molecules::ProtocolScanner
- Defined in:
- lib/ace/support/nav/molecules/protocol_scanner.rb
Overview
Scans for protocol resources using registered sources
Instance Attribute Summary collapse
-
#config_loader ⇒ Object
readonly
Returns the value of attribute config_loader.
Instance Method Summary collapse
- #deduplicate_resources(resources) ⇒ Object
-
#extension_inference_enabled? ⇒ Boolean
Check if extension inference is enabled in settings (cached).
-
#find_resources(protocol, pattern = "*") ⇒ Object
Find resources in all sources for a protocol.
-
#find_resources_in_source(source, protocol, pattern = "*") ⇒ Object
Legacy wrapper method for HandbookScanner compatibility.
-
#find_resources_in_source_internal(source, protocol_config, pattern = "*") ⇒ Object
Find resources in a specific source (internal implementation).
-
#find_resources_with_extensions(source, protocol_config, pattern = "*") ⇒ Object
Find resources using pattern and extensions (original logic).
-
#find_resources_with_inference(source, protocol_config, pattern) ⇒ Object
Find resources using extension inference when exact match fails.
-
#initialize(gem_resolver: nil, path_normalizer: nil, config_loader: nil, extension_inferrer: nil) ⇒ ProtocolScanner
constructor
A new instance of ProtocolScanner.
-
#reset_extension_inference_cache! ⇒ Object
Reset extension inference cache (for testing).
-
#scan_all_sources ⇒ Object
Legacy method for compatibility - get all sources across all protocols.
-
#scan_source_by_alias(alias_name) ⇒ Object
Legacy method - scan source by alias.
-
#sources_for_protocol(protocol) ⇒ Object
Get all sources for a protocol.
Constructor Details
#initialize(gem_resolver: nil, path_normalizer: nil, config_loader: nil, extension_inferrer: nil) ⇒ ProtocolScanner
Returns a new instance of ProtocolScanner.
17 18 19 20 21 22 23 24 |
# File 'lib/ace/support/nav/molecules/protocol_scanner.rb', line 17 def initialize(gem_resolver: nil, path_normalizer: nil, config_loader: nil, extension_inferrer: nil) @gem_resolver = gem_resolver || Atoms::GemResolver.new @path_normalizer = path_normalizer || Atoms::PathNormalizer.new @config_loader = config_loader || ConfigLoader.new # extension_inferrer param kept for backwards compatibility but ignored # ExtensionInferrer now uses class methods @extension_inference_enabled = nil end |
Instance Attribute Details
#config_loader ⇒ Object (readonly)
Returns the value of attribute config_loader.
15 16 17 |
# File 'lib/ace/support/nav/molecules/protocol_scanner.rb', line 15 def config_loader @config_loader end |
Instance Method Details
#deduplicate_resources(resources) ⇒ Object
279 280 281 282 283 284 285 286 287 288 289 |
# File 'lib/ace/support/nav/molecules/protocol_scanner.rb', line 279 def deduplicate_resources(resources) seen = Set.new resources.each_with_object([]) do |resource, unique_resources| key = dedupe_resource_key(resource) next if seen.include?(key) seen.add(key) unique_resources << resource end end |
#extension_inference_enabled? ⇒ Boolean
Check if extension inference is enabled in settings (cached)
292 293 294 295 296 297 298 |
# File 'lib/ace/support/nav/molecules/protocol_scanner.rb', line 292 def extension_inference_enabled? return @extension_inference_enabled unless @extension_inference_enabled.nil? settings = @config_loader.load_settings inference_config = settings["extension_inference"] || {} @extension_inference_enabled = inference_config["enabled"] != false # Default true end |
#find_resources(protocol, pattern = "*") ⇒ Object
Find resources in all sources for a protocol
32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/ace/support/nav/molecules/protocol_scanner.rb', line 32 def find_resources(protocol, pattern = "*") sources = sources_for_protocol(protocol) protocol_config = @config_loader.load_protocol_config(protocol) resources = [] sources.each do |source| next unless source.exists? resources.concat(find_resources_in_source_internal(source, protocol_config, pattern)) end deduplicate_resources(resources) end |
#find_resources_in_source(source, protocol, pattern = "*") ⇒ Object
Legacy wrapper method for HandbookScanner compatibility
306 307 308 309 310 311 312 313 314 315 |
# File 'lib/ace/support/nav/molecules/protocol_scanner.rb', line 306 def find_resources_in_source(source, protocol, pattern = "*") # If second param is a string (protocol name), load its config if protocol.is_a?(String) protocol_config = @config_loader.load_protocol_config(protocol) find_resources_in_source_internal(source, protocol_config, pattern) else # Already a protocol config find_resources_in_source_internal(source, protocol, pattern) end end |
#find_resources_in_source_internal(source, protocol_config, pattern = "*") ⇒ Object
Find resources in a specific source (internal implementation)
48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/ace/support/nav/molecules/protocol_scanner.rb', line 48 def find_resources_in_source_internal(source, protocol_config, pattern = "*") # Try exact match first resources = find_resources_with_extensions(source, protocol_config, pattern) # If no results and extension inference is enabled, try with inferred extensions if resources.empty? && extension_inference_enabled? && !pattern.include?("/") && pattern != "*" resources = find_resources_with_inference(source, protocol_config, pattern) end resources end |
#find_resources_with_extensions(source, protocol_config, pattern = "*") ⇒ Object
Find resources using pattern and extensions (original logic)
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/ace/support/nav/molecules/protocol_scanner.rb', line 61 def find_resources_with_extensions(source, protocol_config, pattern = "*") # Handle both ProtocolSource and HandbookSource objects if source.respond_to?(:full_path) return [] unless source.exists? search_path = source.full_path else # Legacy HandbookSource return [] unless source.exists? search_path = source.handbook_path end # Get extensions from protocol config extensions = protocol_config["extensions"] || [] resources = [] # Check if pattern contains directory structure if pattern.include?("/") # Handle subdirectory patterns if pattern.end_with?("/") # Pattern like "base/" has two interpretations: # 1. Files in a subdirectory named "base" # 2. Files that start with "base" prefix prefix = pattern.chomp("/") # First, try as a subdirectory subdir_path = File.join(search_path, prefix) has_subdir = Dir.exist?(subdir_path) if has_subdir # Subdirectory exists, list files in it found_subdir_paths = Set.new # Track paths to avoid duplicates if extensions.empty? # Match any file in the subdirectory glob_pattern = File.join(subdir_path, "*") glob_pattern_nested = File.join(subdir_path, "**", "*") [glob_pattern, glob_pattern_nested].each do |gp| Dir.glob(gp).each do |file_path| next unless File.file?(file_path) next if found_subdir_paths.include?(file_path) # Skip duplicates found_subdir_paths.add(file_path) resources << create_resource_info(file_path, search_path, source, protocol_config["protocol"]) end end else # Match files with specified extensions in the subdirectory extensions.each do |ext| glob_pattern = File.join(subdir_path, "*#{ext}") glob_pattern_nested = File.join(subdir_path, "**", "*#{ext}") [glob_pattern, glob_pattern_nested].each do |gp| Dir.glob(gp).each do |file_path| next unless File.file?(file_path) next if found_subdir_paths.include?(file_path) # Skip duplicates found_subdir_paths.add(file_path) resources << create_resource_info(file_path, search_path, source, protocol_config["protocol"]) end end end end end # Also try as a prefix pattern (files starting with prefix) found_paths = Set.new # Track paths to avoid duplicates if extensions.empty? # Match files starting with prefix glob_patterns = [ File.join(search_path, "#{prefix}*"), File.join(search_path, "**", "#{prefix}*") ] glob_patterns.each do |gp| Dir.glob(gp).each do |file_path| next unless File.file?(file_path) next if found_paths.include?(file_path) # Skip duplicates found_paths.add(file_path) resources << create_resource_info(file_path, search_path, source, protocol_config["protocol"]) end end else # Match files with specified extensions starting with prefix extensions.each do |ext| glob_patterns = [ File.join(search_path, "#{prefix}*#{ext}"), File.join(search_path, "**", "#{prefix}*#{ext}") ] glob_patterns.each do |gp| Dir.glob(gp).each do |file_path| next unless File.file?(file_path) next if found_paths.include?(file_path) # Skip duplicates found_paths.add(file_path) resources << create_resource_info(file_path, search_path, source, protocol_config["protocol"]) end end end end elsif extensions.empty? # Pattern like "base/*" or "base/something" - use as-is but handle properly glob_pattern = File.join(search_path, pattern) glob_pattern += "*" unless pattern.end_with?("*") || pattern.include?("*") Dir.glob(glob_pattern).each do |file_path| next unless File.file?(file_path) resources << create_resource_info(file_path, search_path, source, protocol_config["protocol"]) end else extensions.each do |ext| glob_pattern = if pattern.end_with?(ext) File.join(search_path, pattern) else File.join(search_path, "#{pattern}#{ext}") end Dir.glob(glob_pattern).each do |file_path| next unless File.file?(file_path) resources << create_resource_info(file_path, search_path, source, protocol_config["protocol"]) end end end elsif extensions.empty? # Original behavior for patterns without directory structure glob_pattern = File.join(search_path, "**", pattern) glob_pattern += "*" unless pattern.end_with?("*") Dir.glob(glob_pattern).each do |file_path| next unless File.file?(file_path) resources << create_resource_info(file_path, search_path, source, protocol_config["protocol"]) end # If no extensions specified, match any file else # Match files with specified extensions extensions.each do |ext| # Check if pattern already ends with this extension glob_pattern = if pattern.end_with?(ext) # Pattern already has extension, search as-is File.join(search_path, "**", pattern) else # Append extension to pattern File.join(search_path, "**", "#{pattern}#{ext}") end Dir.glob(glob_pattern).each do |file_path| next unless File.file?(file_path) resources << create_resource_info(file_path, search_path, source, protocol_config["protocol"]) end end end resources end |
#find_resources_with_inference(source, protocol_config, pattern) ⇒ Object
Find resources using extension inference when exact match fails
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 |
# File 'lib/ace/support/nav/molecules/protocol_scanner.rb', line 219 def find_resources_with_inference(source, protocol_config, pattern) # Handle both ProtocolSource and HandbookSource objects if source.respond_to?(:full_path) return [] unless source.exists? search_path = source.full_path else return [] unless source.exists? search_path = source.handbook_path end # Get configuration for extension inference settings = @config_loader.load_settings inference_config = settings["extension_inference"] || {} enabled = inference_config["enabled"] != false # Default true fallback_order = inference_config["fallback_order"] # Get protocol extensions protocol_extensions = protocol_config["extensions"] || [] inferred_extensions = protocol_config["inferred_extensions"] || protocol_extensions # Generate candidate patterns using extension inferrer candidates = Atoms::ExtensionInferrer.infer_extensions( pattern, protocol_extensions: inferred_extensions, enabled: enabled, fallback_order: fallback_order ) resources = [] found_paths = Set.new # Track paths to avoid duplicates # Try each candidate pattern in order candidates.each do |candidate| # For inference, we need to allow additional extensions after the inferred one # e.g., "mydoc.cst" should match "mydoc.cst.md" # Use brace expansion for tighter matching: exact match or with extension glob_pattern = File.join(search_path, "**", candidate + "{,.*}") Dir.glob(glob_pattern).each do |file_path| next unless File.file?(file_path) next if found_paths.include?(file_path) # Only match if basename equals candidate or has candidate as prefix with dot separator # This prevents "multi-ext.g" from matching "multi-ext.guide.md" basename = File.basename(file_path) basename_candidate = File.basename(candidate) next unless basename == basename_candidate || basename.start_with?(basename_candidate + ".") found_paths.add(file_path) resources << create_resource_info(file_path, search_path, source, protocol_config["protocol"]) end # Stop at first match (DWIM: return first successful inference) break if resources.any? end resources end |
#reset_extension_inference_cache! ⇒ Object
Reset extension inference cache (for testing)
301 302 303 |
# File 'lib/ace/support/nav/molecules/protocol_scanner.rb', line 301 def reset_extension_inference_cache! @extension_inference_enabled = nil end |
#scan_all_sources ⇒ Object
Legacy method for compatibility - get all sources across all protocols
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 |
# File 'lib/ace/support/nav/molecules/protocol_scanner.rb', line 318 def scan_all_sources sources = [] protocols = @config_loader.valid_protocols protocols.each do |protocol| protocol_sources = sources_for_protocol(protocol) # Convert to legacy HandbookSource format for compatibility protocol_sources.each do |source| # The path already points to the handbook directory # HandbookSource will append /handbook if needed base_path = source.full_path # Remove /handbook from the path if present since HandbookSource adds it if base_path.end_with?("/handbook") base_path = File.dirname(base_path) end sources << Models::HandbookSource.new( name: source.name, path: base_path, alias_name: "@#{source.name}", type: source.type.to_sym, priority: source.priority ) end end # Remove duplicates by alias_name sources.uniq { |s| s.alias_name } end |
#scan_source_by_alias(alias_name) ⇒ Object
Legacy method - scan source by alias
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 |
# File 'lib/ace/support/nav/molecules/protocol_scanner.rb', line 351 def scan_source_by_alias(alias_name) # Remove @ prefix if present name = alias_name.start_with?("@") ? alias_name[1..] : alias_name # Handle special aliases case name when "project", "local" return scan_project_source when "user", "global" return scan_user_source end # Find in registered sources protocols = @config_loader.valid_protocols protocols.each do |protocol| source = sources_for_protocol(protocol).find { |s| s.name == name } if source return Models::HandbookSource.new( name: source.name, path: File.dirname(source.full_path), alias_name: "@#{source.name}", type: source.type.to_sym, priority: source.priority ) end end nil end |
#sources_for_protocol(protocol) ⇒ Object
Get all sources for a protocol
27 28 29 |
# File 'lib/ace/support/nav/molecules/protocol_scanner.rb', line 27 def sources_for_protocol(protocol) @config_loader.sources_for_protocol(protocol) end |