Class: DirValidator::Validator

Inherits:
Object
  • Object
show all
Defined in:
lib/dir_validator/validator.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root_path) ⇒ Validator

Returns a new instance of Validator.

Parameters:

  • root_path (String)

    Path to the directory structure to be validated.



17
18
19
20
21
22
# File 'lib/dir_validator/validator.rb', line 17

def initialize(root_path)
  @root_path = root_path
  @catalog   = DirValidator::Catalog.new(self)
  @warnings  = []
  @validated = false
end

Instance Attribute Details

#root_pathString (readonly)

The root path of the validator.

Returns:

  • (String)


8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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
217
218
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
# File 'lib/dir_validator/validator.rb', line 8

class DirValidator::Validator

  attr_reader(:root_path, :warnings)
  attr_reader(:catalog, :validated)  # @!visibility private

  FILE_SEP  = File::SEPARATOR        # @!visibility private
  EXTRA_VID = '_EXTRA_'              # @!visibility private

  # @param  root_path [String] Path to the directory structure to be validated.
  def initialize(root_path)
    @root_path = root_path
    @catalog   = DirValidator::Catalog.new(self)
    @warnings  = []
    @validated = false
  end

  # Validation method. See {file:README.rdoc} for details.
  #
  # Plural validation methods ({#dirs} and {#files}) return an array of
  # {DirValidator::Item} objects. Singular variants ({#dirs} and {#files})
  # return one such object, or nil.
  #
  # @param vid   [String]  Validation identifier meaningful to the user.
  # @param opts  [Hash]    Validation options.
  #
  # @option opts :name    [String]             Item name must match a literal string.
  # @option opts :re      [String|Regexp]      Item name must match regular expression.
  # @option opts :pattern [String]             Item name must match a glob-like pattern.
  # @option opts :n       [String]             Expected number of items. Plural validation
  #                                            methods default to '1+'. Singular variants
  #                                            force the option to be '1'.
  # @option opts :recurse [false|true] (false) Whether to return items other than immediate
  #                                            children.
  #
  # @return   [DirValidator::Item|nil]
  def dir(vid, opts = {})
    opts = opts.merge({:n => '1'})
    return dirs(vid, opts).first
  end

  # @see #dir
  # @return   (see #dir)
  def file(vid, opts = {})
    opts = opts.merge({:n => '1'})
    return files(vid, opts).first
  end

  # @see #dir
  # @return   [Array]
  def dirs(vid, opts = {})
    bd = normalized_base_dir(opts, :handle_recurse => true)
    return process_items(@catalog.unmatched_dirs(bd), vid, opts)
  end

  # @see #dir
  # @return   [Array]
  def files(vid, opts = {})
    bd = normalized_base_dir(opts, :handle_recurse => true)
    return process_items(@catalog.unmatched_files(bd), vid, opts)
  end

  # The workhorse for the the validation methods.
  #
  # @!visibility private
  def process_items(items, vid, opts = {})
    # Make sure the user did not forget to pass the validation identifier.
    if vid.class == Hash
      msg = "Validation identifier should not be a hash: #{vid.inspect}"
      raise ArgumentError, msg
    end

    # Get a Quantifier object.
    quant = DirValidator::Quantity.new(opts[:n] || '1+')

    # We are given a list of unmatched Items (either files or dirs).
    # Here we filter the list to those matching the name-related criteria.
    items = name_filtered(items, opts)

    # And here we cap the N of items to be returned. For example, if the
    # user asked for 1-3 Items and we found 5, we will return only 3.
    items = quantity_limited(items, quant)

    # Add a warning if the N of Items is less than the user's expectation.
    sz = items.size
    unless sz >= quant.min_n
      add_warning(vid, opts.merge(:got => sz))
    end

    # Mark the Items as matched so subsequent validations won't return same Items.
    @catalog.mark_as_matched(items)

    return items
  end


  ####
  # Name-related filtering.
  ####

  # Takes an array of items and a validation-method opts hash.
  # Returns the subset of those items matching the name-related
  # criteria in the opts hash.
  #
  # @!visibility private
  def name_filtered(items, opts)
    # Filter the items to those in the base_dir.
    # If there is no base_dir, no filtering occurs.
    base_dir = normalized_base_dir(opts, :add_file_sep => true)
    sz       = base_dir.size
    items    = items.select { |i| i.path.start_with?(base_dir) } if sz > 0

    # Set the item.target values, which are the values that will
    # be subjected to the name-related test. If there is no base_dir,
    # the target is the same as item.path.
    items.each { |i| i.set_target(i.path[sz .. -1]) }

    # Filter items to immediate children, unless user wants to recurse.
    items = items.reject { |i| i.target.include?(FILE_SEP) } unless opts[:recurse]

    # Return the items having targets matching the name regex.
    rgx = name_regex(opts)
    return items.select { |i| i.target_match(rgx) }
  end

  # Takes a validation-method opts hash.
  # Returns opts[:base_dir] in a normalized form (with trailing separator).
  # Returns nil or '' under certain conditions.
  #
  # @!visibility private
  def normalized_base_dir(opts, nbd_opts = {})
    return nil if opts[:recurse] and nbd_opts[:handle_recurse]
    bd = opts[:base_dir]
    return '' unless bd
    bd.chop! while bd.end_with?(FILE_SEP)
    bd += FILE_SEP if nbd_opts[:add_file_sep]
    return bd
  end

  # Takes a validation-method opts hash.
  # Returns the appropriate Regexp based on the name-related criteria.
  #
  # @!visibility private
  def name_regex(opts)
    name    = opts[:name]
    re      = opts[:re]
    pattern = opts[:pattern]
    return Regexp.new(
      name    ? name_to_re(name)       :
      pattern ? pattern_to_re(pattern) :
      re      ? re                     : ''
    )
  end

  # Takes a validation-method opts[:name] value.
  # Returns the corresponding regex-ready string:
  #   - wrapped in start- and end- anchors
  #   - all special characters quoted
  #
  # @!visibility private
  def name_to_re(name)
    return az_wrap(Regexp.quote(name))
  end

  # Takes a validation-method opts[:pattern] value.
  # Returns the corresponding regex-ready string
  #   - wrapped in start- and end- anchors
  #   - all special regex chacters quoted except for:
  #     * becomes .*
  #     ? becomes .
  #
  # @!visibility private
  def pattern_to_re(pattern)
    return az_wrap(Regexp.quote(pattern).gsub(/\\\*/, '.*').gsub(/\\\?/, '.'))
  end

  # Takes a string.
  # Returns a new string wrapped in Regexp start-of-string and end-of-string anchors.
  #
  # @!visibility private
  def az_wrap(s)
    return "\\A#{s}\\z"
  end


  ####
  # Quantity filtering.
  ####

  # Takes an array of items and a DirValidator::Quantity object.
  # Returns the subset of those items falling within the max allowed
  # by the Quantity.
  #
  # @!visibility private
  def quantity_limited(items, quant)
    return items[0 .. quant.max_index]
  end


  ####
  # Methods related validation warnings and reporting.
  ####

  # Takes a validation identifier and a validation-method opts hash.
  # Creates and new DirValidator::Warning and adds it to the validator's
  # array of warnings.
  #
  # @!visibility private
  def add_warning(vid, opts)
    @warnings << DirValidator::Warning.new(vid, opts)
  end

  # Adds a warning to the validator for all unmatched items in the catalog.
  # Should be run after all validation-methods have been called, typically
  # before producing a report.
  #
  # @!visibility private
  def validate
    return if @validated  # Run only once.
    @catalog.unmatched_items.each do |item|
      add_warning(EXTRA_VID, :path => item.path)
    end
    @validated = true
  end

  # Write a CSV report of the information contained in the validator's warnings.
  #
  # @param io [IO]  IO object to which the report should be written.
  def report(io = STDOUT)
    require 'csv'
    validate()
    report_data.each do |row|
      io.puts CSV.generate_line(row)
    end
  end

  # Returns a matrix of warning information.
  # Used when producing the CSV report.
  #
  # @!visibility private
  def report_data
    rc = DirValidator::Validator.report_columns
    data = [rc]
    @warnings.each do |w|
      cells = rc.map { |c| v = w.opts[c]; v.nil? ? '' : v }
      cells[0] = w.vid
      data.push(cells)
    end
    return data
  end

  # Column headings for the CSV report.
  #
  # @!visibility private
  def self.report_columns
    return [:vid, :got, :n, :base_dir, :name, :re, :pattern, :path]
  end

end

#warningsArray (readonly)

The validator’s Warning objects.

Returns:

  • (Array)


8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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
217
218
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
# File 'lib/dir_validator/validator.rb', line 8

class DirValidator::Validator

  attr_reader(:root_path, :warnings)
  attr_reader(:catalog, :validated)  # @!visibility private

  FILE_SEP  = File::SEPARATOR        # @!visibility private
  EXTRA_VID = '_EXTRA_'              # @!visibility private

  # @param  root_path [String] Path to the directory structure to be validated.
  def initialize(root_path)
    @root_path = root_path
    @catalog   = DirValidator::Catalog.new(self)
    @warnings  = []
    @validated = false
  end

  # Validation method. See {file:README.rdoc} for details.
  #
  # Plural validation methods ({#dirs} and {#files}) return an array of
  # {DirValidator::Item} objects. Singular variants ({#dirs} and {#files})
  # return one such object, or nil.
  #
  # @param vid   [String]  Validation identifier meaningful to the user.
  # @param opts  [Hash]    Validation options.
  #
  # @option opts :name    [String]             Item name must match a literal string.
  # @option opts :re      [String|Regexp]      Item name must match regular expression.
  # @option opts :pattern [String]             Item name must match a glob-like pattern.
  # @option opts :n       [String]             Expected number of items. Plural validation
  #                                            methods default to '1+'. Singular variants
  #                                            force the option to be '1'.
  # @option opts :recurse [false|true] (false) Whether to return items other than immediate
  #                                            children.
  #
  # @return   [DirValidator::Item|nil]
  def dir(vid, opts = {})
    opts = opts.merge({:n => '1'})
    return dirs(vid, opts).first
  end

  # @see #dir
  # @return   (see #dir)
  def file(vid, opts = {})
    opts = opts.merge({:n => '1'})
    return files(vid, opts).first
  end

  # @see #dir
  # @return   [Array]
  def dirs(vid, opts = {})
    bd = normalized_base_dir(opts, :handle_recurse => true)
    return process_items(@catalog.unmatched_dirs(bd), vid, opts)
  end

  # @see #dir
  # @return   [Array]
  def files(vid, opts = {})
    bd = normalized_base_dir(opts, :handle_recurse => true)
    return process_items(@catalog.unmatched_files(bd), vid, opts)
  end

  # The workhorse for the the validation methods.
  #
  # @!visibility private
  def process_items(items, vid, opts = {})
    # Make sure the user did not forget to pass the validation identifier.
    if vid.class == Hash
      msg = "Validation identifier should not be a hash: #{vid.inspect}"
      raise ArgumentError, msg
    end

    # Get a Quantifier object.
    quant = DirValidator::Quantity.new(opts[:n] || '1+')

    # We are given a list of unmatched Items (either files or dirs).
    # Here we filter the list to those matching the name-related criteria.
    items = name_filtered(items, opts)

    # And here we cap the N of items to be returned. For example, if the
    # user asked for 1-3 Items and we found 5, we will return only 3.
    items = quantity_limited(items, quant)

    # Add a warning if the N of Items is less than the user's expectation.
    sz = items.size
    unless sz >= quant.min_n
      add_warning(vid, opts.merge(:got => sz))
    end

    # Mark the Items as matched so subsequent validations won't return same Items.
    @catalog.mark_as_matched(items)

    return items
  end


  ####
  # Name-related filtering.
  ####

  # Takes an array of items and a validation-method opts hash.
  # Returns the subset of those items matching the name-related
  # criteria in the opts hash.
  #
  # @!visibility private
  def name_filtered(items, opts)
    # Filter the items to those in the base_dir.
    # If there is no base_dir, no filtering occurs.
    base_dir = normalized_base_dir(opts, :add_file_sep => true)
    sz       = base_dir.size
    items    = items.select { |i| i.path.start_with?(base_dir) } if sz > 0

    # Set the item.target values, which are the values that will
    # be subjected to the name-related test. If there is no base_dir,
    # the target is the same as item.path.
    items.each { |i| i.set_target(i.path[sz .. -1]) }

    # Filter items to immediate children, unless user wants to recurse.
    items = items.reject { |i| i.target.include?(FILE_SEP) } unless opts[:recurse]

    # Return the items having targets matching the name regex.
    rgx = name_regex(opts)
    return items.select { |i| i.target_match(rgx) }
  end

  # Takes a validation-method opts hash.
  # Returns opts[:base_dir] in a normalized form (with trailing separator).
  # Returns nil or '' under certain conditions.
  #
  # @!visibility private
  def normalized_base_dir(opts, nbd_opts = {})
    return nil if opts[:recurse] and nbd_opts[:handle_recurse]
    bd = opts[:base_dir]
    return '' unless bd
    bd.chop! while bd.end_with?(FILE_SEP)
    bd += FILE_SEP if nbd_opts[:add_file_sep]
    return bd
  end

  # Takes a validation-method opts hash.
  # Returns the appropriate Regexp based on the name-related criteria.
  #
  # @!visibility private
  def name_regex(opts)
    name    = opts[:name]
    re      = opts[:re]
    pattern = opts[:pattern]
    return Regexp.new(
      name    ? name_to_re(name)       :
      pattern ? pattern_to_re(pattern) :
      re      ? re                     : ''
    )
  end

  # Takes a validation-method opts[:name] value.
  # Returns the corresponding regex-ready string:
  #   - wrapped in start- and end- anchors
  #   - all special characters quoted
  #
  # @!visibility private
  def name_to_re(name)
    return az_wrap(Regexp.quote(name))
  end

  # Takes a validation-method opts[:pattern] value.
  # Returns the corresponding regex-ready string
  #   - wrapped in start- and end- anchors
  #   - all special regex chacters quoted except for:
  #     * becomes .*
  #     ? becomes .
  #
  # @!visibility private
  def pattern_to_re(pattern)
    return az_wrap(Regexp.quote(pattern).gsub(/\\\*/, '.*').gsub(/\\\?/, '.'))
  end

  # Takes a string.
  # Returns a new string wrapped in Regexp start-of-string and end-of-string anchors.
  #
  # @!visibility private
  def az_wrap(s)
    return "\\A#{s}\\z"
  end


  ####
  # Quantity filtering.
  ####

  # Takes an array of items and a DirValidator::Quantity object.
  # Returns the subset of those items falling within the max allowed
  # by the Quantity.
  #
  # @!visibility private
  def quantity_limited(items, quant)
    return items[0 .. quant.max_index]
  end


  ####
  # Methods related validation warnings and reporting.
  ####

  # Takes a validation identifier and a validation-method opts hash.
  # Creates and new DirValidator::Warning and adds it to the validator's
  # array of warnings.
  #
  # @!visibility private
  def add_warning(vid, opts)
    @warnings << DirValidator::Warning.new(vid, opts)
  end

  # Adds a warning to the validator for all unmatched items in the catalog.
  # Should be run after all validation-methods have been called, typically
  # before producing a report.
  #
  # @!visibility private
  def validate
    return if @validated  # Run only once.
    @catalog.unmatched_items.each do |item|
      add_warning(EXTRA_VID, :path => item.path)
    end
    @validated = true
  end

  # Write a CSV report of the information contained in the validator's warnings.
  #
  # @param io [IO]  IO object to which the report should be written.
  def report(io = STDOUT)
    require 'csv'
    validate()
    report_data.each do |row|
      io.puts CSV.generate_line(row)
    end
  end

  # Returns a matrix of warning information.
  # Used when producing the CSV report.
  #
  # @!visibility private
  def report_data
    rc = DirValidator::Validator.report_columns
    data = [rc]
    @warnings.each do |w|
      cells = rc.map { |c| v = w.opts[c]; v.nil? ? '' : v }
      cells[0] = w.vid
      data.push(cells)
    end
    return data
  end

  # Column headings for the CSV report.
  #
  # @!visibility private
  def self.report_columns
    return [:vid, :got, :n, :base_dir, :name, :re, :pattern, :path]
  end

end

Instance Method Details

#dir(vid, opts = {}) ⇒ DirValidator::Item|nil

Validation method. See README for details.

Plural validation methods (#dirs and #files) return an array of Item objects. Singular variants (#dirs and #files) return one such object, or nil.

Parameters:

  • vid (String)

    Validation identifier meaningful to the user.

  • opts (Hash) (defaults to: {})

    Validation options.

Options Hash (opts):

  • :name (String)

    Item name must match a literal string.

  • :re (String|Regexp)

    Item name must match regular expression.

  • :pattern (String)

    Item name must match a glob-like pattern.

  • :n (String)

    Expected number of items. Plural validation methods default to ‘1+’. Singular variants force the option to be ‘1’.

  • :recurse (false|true) — default: false

    Whether to return items other than immediate children.

Returns:



43
44
45
46
# File 'lib/dir_validator/validator.rb', line 43

def dir(vid, opts = {})
  opts = opts.merge({:n => '1'})
  return dirs(vid, opts).first
end

#dirs(vid, opts = {}) ⇒ Array

Returns:

  • (Array)

See Also:



57
58
59
60
# File 'lib/dir_validator/validator.rb', line 57

def dirs(vid, opts = {})
  bd = normalized_base_dir(opts, :handle_recurse => true)
  return process_items(@catalog.unmatched_dirs(bd), vid, opts)
end

#file(vid, opts = {}) ⇒ DirValidator::Item|nil

Returns:

See Also:



50
51
52
53
# File 'lib/dir_validator/validator.rb', line 50

def file(vid, opts = {})
  opts = opts.merge({:n => '1'})
  return files(vid, opts).first
end

#files(vid, opts = {}) ⇒ Array

Returns:

  • (Array)

See Also:



64
65
66
67
# File 'lib/dir_validator/validator.rb', line 64

def files(vid, opts = {})
  bd = normalized_base_dir(opts, :handle_recurse => true)
  return process_items(@catalog.unmatched_files(bd), vid, opts)
end

#report(io = STDOUT) ⇒ Object

Write a CSV report of the information contained in the validator’s warnings.

Parameters:

  • io (IO) (defaults to: STDOUT)

    IO object to which the report should be written.



235
236
237
238
239
240
241
# File 'lib/dir_validator/validator.rb', line 235

def report(io = STDOUT)
  require 'csv'
  validate()
  report_data.each do |row|
    io.puts CSV.generate_line(row)
  end
end