-
Notifications
You must be signed in to change notification settings - Fork 264
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Begin outlining the design for multiple scanners
* Scanner protocol. * ScannerMultiplexer. * FileProvider. Refs #163
- Loading branch information
Showing
4 changed files
with
187 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
module I18n::Tasks::Scanners | ||
# Finds the files and provides their source. | ||
# | ||
# @note This class is thread-safe. All methods are cached. | ||
# @since 0.9.0 | ||
class FileProvider | ||
include I18n::Tasks::Logging | ||
|
||
# @param search_paths [Array<String>] {Find.find}-compatible paths to traverse, | ||
# absolute or relative to the working directory. | ||
# @param include [Array<String>, nil] {File.fnmatch}-compatible patterns files to include. | ||
# Files not matching any of the inclusion patterns will be excluded. | ||
# @param exclude [Arry<String>] {File.fnmatch}-compatible patterns of files to exclude. | ||
# Files matching any of the exclusion patterns will be excluded even if they match an inclusion pattern. | ||
def initialize(search_paths: [], include: nil, exclude: []) | ||
@search_paths = search_paths | ||
@include = include | ||
@exclude = exclude | ||
|
||
@file_cache = {} | ||
@mutex = Mutex.new | ||
end | ||
|
||
# Traverse the paths and yield the matching ones. | ||
# | ||
# @note This method is cached, it will only access the filesystem on the first invocation. | ||
# @yield [path] | ||
# @yieldparam path [String] the path of the found file. | ||
# @return [Array<of block results>] | ||
def traverse_files | ||
find_paths.map { |path| yield path } | ||
end | ||
|
||
# Return the contents of the file at the given path. | ||
# The file is read in the 'rb' mode and assuming UTF-8 encoding. | ||
# | ||
# @note This method is cached, it will only access the filesystem on the first invocation. | ||
# @param [String] path Path to the file, absolute or relative to the working directory. | ||
def read_file(path) | ||
absolute_path = File.expand_path(path) | ||
contents = @file_cache[absolute_path] | ||
return contents if contents | ||
@mutex.synchronize do | ||
@file_cache[absolute_path] ||= begin | ||
result = nil | ||
File.open(absolute_path, 'rb', encoding: 'UTF-8') { |f| result = f.read } | ||
result | ||
end | ||
end | ||
end | ||
|
||
private | ||
|
||
# @param path [String] | ||
# @param globs [Array<String>] | ||
# @return [Boolean] | ||
def path_fnmatch_any?(path, globs) | ||
globs.any? { |glob| File.fnmatch(glob, path) } | ||
end | ||
|
||
# @return [Array<String>] | ||
def find_paths | ||
return @paths if @paths | ||
@mutex.synchronize do | ||
@paths ||= begin | ||
paths = [] | ||
search_paths = @search_paths.select { |p| File.exist?(p) } | ||
if search_paths.empty? | ||
log_warn "None of the search.paths exist #{@search_paths.inspect}" | ||
else | ||
Find.find(*search_paths) do |path| | ||
is_dir = File.directory?(path) | ||
hidden = File.basename(path).start_with?('.') | ||
not_incl = @include && !path_fnmatch_any?(path, @include) | ||
excl = path_fnmatch_any?(path, @exclude) | ||
if is_dir || hidden || not_incl || excl | ||
Find.prune if is_dir && (hidden || excl) | ||
else | ||
paths << path | ||
end | ||
end | ||
end | ||
paths | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
module I18n::Tasks::Scanners | ||
# Describes the API of a scanner. | ||
# | ||
# @abstract | ||
# @since 0.9.0 | ||
class Scanner | ||
# @abstract | ||
# @return [Array<KeyOccurrences>] the keys found by this scanner and their occurrences. | ||
def keys | ||
raise 'Unimplemented' | ||
end | ||
end | ||
|
||
# A scanned key and all its occurrences. | ||
class KeyOccurrences | ||
# @return [String] the key. | ||
attr_reader :key | ||
|
||
# @return [Array<Occurrence>] the key's occurrences. | ||
attr_reader :occurrences | ||
|
||
def initialize(key:, occurrences:) | ||
@key = key | ||
@occurrences = occurrences | ||
end | ||
end | ||
|
||
# The occurrence of some key in a file. | ||
class Occurrence | ||
# @return [String] source path relative to the current working directory. | ||
attr_reader :path | ||
|
||
# @return [Fixnum] count of characters in the file before the occurrence. | ||
attr_reader :pos | ||
|
||
# @return [Fixnum] line number of the occurrence, counting from 1. | ||
attr_reader :line_num | ||
|
||
# @return [Fixnum] position of the start of the occurrence in the line, counting from 1. | ||
attr_reader :line_pos | ||
|
||
# @return [String] the line of the occurrence, excluding the last LF or CRLF. | ||
attr_reader :line | ||
|
||
# @param path [String] | ||
# @param pos [Fixnum] | ||
# @param line_num [Fixnum] | ||
# @param line_pos [Fixnum] | ||
# @param line [String] | ||
def initialize(path:, pos:, line_num:, line_pos:, line:) | ||
@path = path | ||
@pos = pos | ||
@line_num = line_num | ||
@line_pos = line_pos | ||
@line = line | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
module I18n::Tasks::Scanners | ||
# Run multiple {Scanner Scanners} and merge their results. | ||
# @note The scanners are run concurrently. A thread is spawned per each scanner. | ||
# @since 0.9.0 | ||
class ScannerMultiplexer < Scanner | ||
# @param scanners [Array<Scanner>] | ||
def initialize(scanners:) | ||
@scanners = scanners | ||
end | ||
|
||
# Collect the results of all the scanners. Occurrences of a key from multiple scanners are merged. | ||
# | ||
# @note The scanners are run concurrently. A thread is spawned per each scanner. | ||
# @return (see Scanner#keys) | ||
def keys | ||
collect_results.inject({}) { |results_by_key, key_occurences| | ||
key_occurences.each do |key_occurrence| | ||
(results_by_key[key_occurrence.key] ||= []) << key_occurrence.occurrences | ||
end | ||
results_by_key | ||
}.map { |key, all_occurrences| | ||
KeyOccurrences.new(key: key, occurrences: all_occurrences.flatten(1)) | ||
} | ||
end | ||
|
||
private | ||
|
||
# @return Array<Array<KeyOccurrences>> | ||
def collect_results | ||
Array.new(@scanners.length).tap do |results| | ||
@scanners.map.with_index { |scanner, i| | ||
Thread.start { results[i] = scanner.keys } | ||
}.each(&:join) | ||
end | ||
end | ||
end | ||
end |