dir_validator 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ == 0.12.0 (2012-07-27)
2
+
3
+ Initial beta release.
data/LICENSE.rdoc ADDED
@@ -0,0 +1,28 @@
1
+ Copyright (c) 2012 by The Board of Trustees of the Leland Stanford Junior
2
+ University. All rights reserved.
3
+
4
+ Redistribution and use of this distribution in source and binary forms, with or
5
+ without modification, are permitted provided that:
6
+
7
+ * The above copyright notice and this permission notice appear in all copies and
8
+ supporting documentation.
9
+
10
+ * The name, identifiers, and trademarks of The Board of Trustees of the Leland
11
+ Stanford Junior University are not used in advertising or publicity without the
12
+ express prior written permission of The Board of Trustees of the Leland
13
+ Stanford Junior University.
14
+
15
+ * Recipients acknowledge that this distribution is made available as a research
16
+ courtesy, "as is", potentially with defects, without any obligation on the part
17
+ of The Board of Trustees of the Leland Stanford Junior University to provide
18
+ support, services, or repair.
19
+
20
+ THE BOARD OF TRUSTEES OF THE LELAND STANFORD JUNIOR UNIVERSITY DISCLAIMS ALL
21
+ WARRANTIES, EXPRESS OR IMPLIED, WITH REGARD TO THIS SOFTWARE, INCLUDING WITHOUT
22
+ LIMITATION ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
23
+ PARTICULAR PURPOSE, AND IN NO EVENT SHALL THE BOARD OF TRUSTEES OF THE LELAND
24
+ STANFORD JUNIOR UNIVERSITY BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL
25
+ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
26
+ WHETHER IN AN ACTION OF CONTRACT, TORT (INCLUDING NEGLIGENCE) OR STRICT
27
+ LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
28
+ SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,134 @@
1
+ = dir_validate
2
+
3
+ == Synopsis
4
+
5
+ require 'dir_validator'
6
+ dv = DirValidator.new('some/path')
7
+ dv.dirs('blah', :pattern => '*').each do |subdir|
8
+ # More validations as needed.
9
+ end
10
+ dv.report()
11
+
12
+
13
+ == Overview
14
+
15
+ This gem provides a convenient syntax for checking whether the contents of a
16
+ directory structure match your expectations.
17
+
18
+ The best place to start is by reading the {file:tutorial/tutorial.rb tutorial script}.
19
+
20
+ The public API for the gem is fairly simple. First you set up the validator by
21
+ passing in the path to the directory structure that you want to check.
22
+
23
+ require 'dir_validator'
24
+ dv = DirValidator.new('some/path')
25
+
26
+ Then you define your expectations regarding the content of the directory
27
+ structure. This is done with the four methods used to declare particular
28
+ validations:
29
+
30
+ dirs()
31
+ files()
32
+ dir()
33
+ file()
34
+
35
+ The validation methods use this syntax:
36
+
37
+ dirs(VID, OPTS)
38
+
39
+ # where VID = The validation identifier.
40
+ # Can be any string meaningful to the user.
41
+ # Used when reporting warnings.
42
+ #
43
+ # OPTS = A hash of options to set up expectations regarding
44
+ # the names of files or directory as well as their
45
+ # quantity.
46
+
47
+ The plural validation methods return an array of {DirValidator::Item} objects.
48
+ The singular variants return one such object (or nil). The returned objects are
49
+ those that (a) meet the criteria specified in the OPTS hash, and (b) have not
50
+ been matched already by prior validations.
51
+
52
+ The validation criteria defined in the OPTS hash come in three general types:
53
+
54
+ * Name-related criteria affect whether a particular {DirValidator::Item} will be
55
+ returned (only if its file or directory name matches the criteria). You can
56
+ supply one of the following:
57
+
58
+ :name # A literal string.
59
+ :re # A regular expression, supplied as either a Regexp or String.
60
+ :pattern # A glob-like pattern. Supports only the * and ? wildcards.
61
+
62
+ # Also see the :recurse option below.
63
+
64
+ * Quantity assertions. These control the maximum number of {DirValidator::Item}
65
+ objects that will be returned. They also generate a warning if too few are
66
+ found.
67
+
68
+ :n # A string in one of the following forms:
69
+ # '*' Zero or more.
70
+ # '+' One or more.
71
+ # '?' Zero or one.
72
+ # 'n+' n or more
73
+ # 'n' Exactly n.
74
+ # 'm-n' m through n, inclusive.
75
+
76
+ * Other attributes:
77
+
78
+ :recurse # Boolean (default = false).
79
+
80
+ # Normally, validation methods find only the immediate
81
+ # children of the object upon which the method is called. If
82
+ # :recurse is true, items deeper in the hierarchy can be
83
+ # discovered.
84
+
85
+ # For example, given this content:
86
+ # some/path/
87
+ # foo/
88
+ # bar/
89
+ # foo/
90
+
91
+ # And this validator.
92
+ dv = DirValidator.new('some/path')
93
+
94
+ # This call would return only the 'foo' directory.
95
+ dv.dirs('a', :re => /foo$/)
96
+
97
+ # Whereas this call would return both 'foo' and 'foo/bar/foo'.
98
+ dv.dirs('a', :re => /foo$/, :recurse => true)
99
+
100
+ After the validations have been defined, you can examine the results programmatically:
101
+
102
+ dv.validate()
103
+ dv.warnings.each do |w|
104
+ puts w.vid
105
+ puts w.opts.inspect
106
+ end
107
+
108
+ More commonly, you can print the information contained in the warnings in the form
109
+ of a basic CSV repot.
110
+
111
+ dv.report()
112
+
113
+ The warnings and the CSV report contain the following information:
114
+
115
+ vid # Validation identifier, as discussed above.
116
+ got # The number of items found.
117
+ n # A normalized version of the :n optionn discussed above.
118
+ base_dir # The parent directory of the item.
119
+ name # The name-related options discussed above.
120
+ re # "
121
+ pattern # "
122
+ path # The path of the item.
123
+
124
+
125
+ == Known issues
126
+
127
+ Currently handles only regular files and directories.
128
+
129
+ Not yet tested on Windows.
130
+
131
+
132
+ == Copyright
133
+
134
+ Copyright © 2012 Stanford University Library. See LICENSE for details.
@@ -0,0 +1,115 @@
1
+ # A Validator has one Catalog, which contains all of the information
2
+ # about the Item objects discovered in the Validator.root_dir.
3
+ #
4
+ # @!visibility private
5
+ class DirValidator::Catalog
6
+
7
+ FS = '\\' + File::SEPARATOR
8
+ DOTDIR = '\.\.?\z' # A dotdir is . or .. at end-of-string,
9
+ DOTDIR_RE = / ( \A | #{FS} ) #{DOTDIR} /x # preceded by start-of-string or file-sep.
10
+
11
+ # Takes a validator, because we need to pass the validator down
12
+ # to the Item objects that are created.
13
+ def initialize(validator)
14
+ # The validator and an array of all Items in the Catalog.
15
+ @validator = validator
16
+ @items = nil
17
+
18
+ # Two indexes of to boost the performance of the unmatched_items method.
19
+ # Both indexes contain the catalog_id values of an remaining unmatched
20
+ # Items. The catalog_id values also serve as indexes into the @items
21
+ # array. The indexes have the following structure:
22
+ #
23
+ # @unmatched[Item.catalog_id] = true
24
+ # @bdi[Item.base_dir][Item.catalog_id] = true
25
+ #
26
+ # The @bdi index is more fined-grained and thus gives the largest
27
+ # performance boost. For typical uses, calls to unmatched_items()
28
+ # will have a base_dir, so we can use @bdi.
29
+ @unmatched = {}
30
+ @bdi = {}
31
+ end
32
+
33
+ # Returns the discovered Items, loading them if needed.
34
+ def items
35
+ return @items ||= load_items()
36
+ end
37
+
38
+ # Scans the Validator.root_dir, creating a new Item for each file/dir
39
+ # found, and adding the Item to the indexes used by the Catalog.
40
+ def load_items
41
+ catalog_id = -1
42
+ @items = []
43
+ Dir.chdir(@validator.root_path) do
44
+ Dir.glob('**/*', File::FNM_DOTMATCH).each do |path|
45
+ # We want dotfiles, but not the . and .. dirs.
46
+ next if path_is_dot_dir(path)
47
+ # Create the new Item, and give it a unique ID, which is
48
+ # also an index into the @items array.
49
+ catalog_id += 1
50
+ item = DirValidator::Item.new(@validator, path, catalog_id)
51
+ @items << item
52
+ # Add Item to the indexes.
53
+ add_to_index(item)
54
+ end
55
+ end
56
+ return @items
57
+ end
58
+
59
+ # Takes a path as a string. Returns true if it's . or .. directory.
60
+ def path_is_dot_dir(path)
61
+ return path =~ DOTDIR_RE ? true : false
62
+ end
63
+
64
+ # Takes an Item object and adds it to the Catalog indexes.
65
+ def add_to_index(item)
66
+ cid = item.catalog_id
67
+ dn = item.dirname2
68
+ @bdi[dn] ||= {}
69
+ @bdi[dn][cid] = true
70
+ @unmatched[cid] = true
71
+ end
72
+
73
+ # Takes and Item and removes it from the Catalog indexes.
74
+ def delete_from_index(item)
75
+ cid = item.catalog_id
76
+ dn = item.dirname2
77
+ @bdi[dn].delete(cid)
78
+ @unmatched.delete(cid)
79
+ end
80
+
81
+ # Returns unmatched directories from the Catalog.
82
+ def unmatched_dirs(base_dir = nil)
83
+ return unmatched_items(base_dir).select { |i| i.is_dir }
84
+ end
85
+
86
+ # Returns unmatched files from the Catalog.
87
+ def unmatched_files(base_dir = nil)
88
+ return unmatched_items(base_dir).select { |i| i.is_file }
89
+ end
90
+
91
+ # Returns unmatches files and directories from the Catalog.
92
+ def unmatched_items(base_dir = nil)
93
+ # If a base_dir is given, we'll use the @bdi index. Otherwise,
94
+ # we'll use the @unmatched index. When using @bdi, we also
95
+ # must return [] if @bdi doesn't contain base_dir as a key.
96
+ # We sort the catalog_id values to obtain a deterministic ordering.
97
+ itms = items()
98
+ h = base_dir ? @bdi[base_dir] : @unmatched
99
+ return [] unless h
100
+ return h.keys.sort.map { |cid| itms[cid] }
101
+ end
102
+
103
+ # Takes an array of Items. Marks them as matched and removes them
104
+ # from the Catalog indexes. We can't do this within unmatched_items()
105
+ # because we don't know that time which of the returned Items will
106
+ # survive the validation-methods name-filtering and quantity-filtering
107
+ # criteria.
108
+ def mark_as_matched(matched_items)
109
+ matched_items.each do |i|
110
+ i.mark_as_matched
111
+ delete_from_index(i)
112
+ end
113
+ end
114
+
115
+ end
@@ -0,0 +1,119 @@
1
+ require 'pathname'
2
+
3
+ # @!attribute [r] path
4
+ # @return [String]
5
+ # The path to the Item, omitting the root_dir of the {DirValidator::Validator}.
6
+ #
7
+ # @!attribute [r] dirname
8
+ # @return [String]
9
+ # The path to the parent directory of the Item, or '.' if the parent
10
+ # directory is the root_dir of the {DirValidator::Validator}.
11
+ #
12
+ # @!attribute [r] match_data
13
+ # @return [MatchData]
14
+ # The MatchData from the most recent regular expression test against the Item.
15
+ class DirValidator::Item
16
+
17
+ attr_reader(:path, :dirname, :match_data)
18
+ attr_reader(:pathname, :dirname2, :catalog_id, :matched, :target, :filetype) # @!visibility private
19
+
20
+ # Returns a new Item, based on these arguments:
21
+ # - Validator.
22
+ # - String: path to a file or directory (omitting Validator.root_dir).
23
+ # - Integer: the Catalog ID assigned by the Catalog class.
24
+ #
25
+ # @!visibility private
26
+ def initialize(validator, path, catalog_id)
27
+ @validator = validator
28
+ @pathname = Pathname.new(path).cleanpath
29
+ @path = @pathname.to_s
30
+ @dirname = @pathname.dirname.to_s
31
+ @dirname2 = @dirname == '.' ? '' : @dirname
32
+ @catalog_id = catalog_id
33
+ @matched = false
34
+ @target = nil
35
+ @match_data = nil
36
+ @filetype = @pathname.file? ? :file :
37
+ @pathname.directory? ? :dir : nil
38
+ end
39
+
40
+ # Just a front-end for Ruby's File#basename.
41
+ #
42
+ # @param args See File#basename.
43
+ # @return [String] The basename of the Item.
44
+ def basename(*args)
45
+ return @pathname.basename(*args).to_s
46
+ end
47
+
48
+ # @!visibility private
49
+ def is_dir
50
+ return @filetype == :dir
51
+ end
52
+
53
+ # @!visibility private
54
+ def is_file
55
+ return @filetype == :file
56
+ end
57
+
58
+ # @!visibility private
59
+ def mark_as_matched
60
+ @matched = true
61
+ end
62
+
63
+ # @!visibility private
64
+ def set_target(t)
65
+ @target = t
66
+ end
67
+
68
+ # Takes a Regexp. Trys to match Item.target against the Regexp.
69
+ # Stores the resulting MatchData.
70
+ #
71
+ # @!visibility private
72
+ def target_match(regex)
73
+ @match_data = regex.match(@target)
74
+ return @match_data
75
+ end
76
+
77
+ # Validation method, using a {DirValidator::Item} as the receiver.
78
+ #
79
+ # @see DirValidator::Validator#dir
80
+ # @return (see DirValidator::Validator#dir)
81
+ def dir(vid, opts = {})
82
+ return @validator.dir(vid, item_opts(opts))
83
+ end
84
+
85
+ # @see #dir
86
+ # @return (see DirValidator::Validator#file)
87
+ def file(vid, opts = {})
88
+ return @validator.file(vid, item_opts(opts))
89
+ end
90
+
91
+ # @see #dir
92
+ # @return (see DirValidator::Validator#dirs)
93
+ def dirs(vid, opts = {})
94
+ return @validator.dirs(vid, item_opts(opts))
95
+ end
96
+
97
+ # @see #dir
98
+ # @return (see DirValidator::Validator#files)
99
+ def files(vid, opts = {})
100
+ return @validator.files(vid, item_opts(opts))
101
+ end
102
+
103
+ # Takes a validation-method opts hash.
104
+ # Returns a new hash of opts with the appropriate value
105
+ # for :base_dir. That value depends on whether the current Item
106
+ # is a dir or file, and whether the file has a parent dir.
107
+ #
108
+ # @!visibility private
109
+ def item_opts(opts)
110
+ if is_dir
111
+ return opts.merge(:base_dir => @path)
112
+ elsif @dirname == '.'
113
+ return opts
114
+ else
115
+ return opts.merge(:base_dir => @dirname)
116
+ end
117
+ end
118
+
119
+ end
@@ -0,0 +1,59 @@
1
+ # @!visibility private
2
+ class DirValidator::Quantity
3
+
4
+ attr_reader(:spec, :min_n, :max_n, :max_index)
5
+
6
+ # Takes a string and returns a new DirValidator::Quantity object.
7
+ # min_n Minimum expected N of items.
8
+ # max_n Maximum N of items to be matched.
9
+ # max_index Array index corresponding to max_n
10
+ def initialize(spec)
11
+ @spec = spec
12
+ parse_spec
13
+ end
14
+
15
+ def parse_spec
16
+ case @spec
17
+ when '*'
18
+ # 0+.
19
+ @min_n = 0
20
+ @max_n = 1.0 / 0
21
+ @max_index = -1
22
+ when '+'
23
+ # 1+.
24
+ @min_n = 1
25
+ @max_n = 1.0 / 0
26
+ @max_index = -1
27
+ when '?'
28
+ # Zero or one.
29
+ @min_n = 0
30
+ @max_n = 1
31
+ @max_index = @max_n - 1
32
+ when /\A (\d+)\+ \z/x
33
+ # n+
34
+ @min_n = $1.to_i
35
+ @max_n = 1.0 / 0
36
+ @max_index = -1
37
+ when /\A (\d+) \z/x
38
+ # n
39
+ @min_n = $1.to_i
40
+ @max_n = @min_n
41
+ @max_index = @max_n - 1
42
+ when /\A (\d+) - (\d+) \z/x
43
+ # m-n
44
+ @min_n = $1.to_i
45
+ @max_n = $2.to_i
46
+ @max_index = @max_n - 1
47
+ else
48
+ invalid_spec()
49
+ end
50
+
51
+ # Sanity check min and max.
52
+ invalid_spec() if (@min_n > @max_n or @min_n < 0)
53
+ end
54
+
55
+ def invalid_spec
56
+ raise ArgumentError, "Invalid quantitifer: #{@spec.inspect}."
57
+ end
58
+
59
+ end
@@ -0,0 +1,265 @@
1
+ # @!attribute [r] root_path
2
+ # @return [String]
3
+ # The root path of the validator.
4
+ #
5
+ # @!attribute [r] warnings
6
+ # @return [Array]
7
+ # The validator's {DirValidator::Warning} objects.
8
+ class DirValidator::Validator
9
+
10
+ attr_reader(:root_path, :warnings)
11
+ attr_reader(:catalog, :validated) # @!visibility private
12
+
13
+ FILE_SEP = File::SEPARATOR # @!visibility private
14
+ EXTRA_VID = '_EXTRA_' # @!visibility private
15
+
16
+ # @param root_path [String] Path to the directory structure to be validated.
17
+ def initialize(root_path)
18
+ @root_path = root_path
19
+ @catalog = DirValidator::Catalog.new(self)
20
+ @warnings = []
21
+ @validated = false
22
+ end
23
+
24
+ # Validation method. See {file:README.rdoc} for details.
25
+ #
26
+ # Plural validation methods ({#dirs} and {#files}) return an array of
27
+ # {DirValidator::Item} objects. Singular variants ({#dirs} and {#files})
28
+ # return one such object, or nil.
29
+ #
30
+ # @param vid [String] Validation identifier meaningful to the user.
31
+ # @param opts [Hash] Validation options.
32
+ #
33
+ # @option opts :name [String] Item name must match a literal string.
34
+ # @option opts :re [String|Regexp] Item name must match regular expression.
35
+ # @option opts :pattern [String] Item name must match a glob-like pattern.
36
+ # @option opts :n [String] Expected number of items. Plural validation
37
+ # methods default to '1+'. Singular variants
38
+ # force the option to be '1'.
39
+ # @option opts :recurse [false|true] (false) Whether to return items other than immediate
40
+ # children.
41
+ #
42
+ # @return [DirValidator::Item|nil]
43
+ def dir(vid, opts = {})
44
+ opts = opts.merge({:n => '1'})
45
+ return dirs(vid, opts).first
46
+ end
47
+
48
+ # @see #dir
49
+ # @return (see #dir)
50
+ def file(vid, opts = {})
51
+ opts = opts.merge({:n => '1'})
52
+ return files(vid, opts).first
53
+ end
54
+
55
+ # @see #dir
56
+ # @return [Array]
57
+ def dirs(vid, opts = {})
58
+ bd = normalized_base_dir(opts, :handle_recurse => true)
59
+ return process_items(@catalog.unmatched_dirs(bd), vid, opts)
60
+ end
61
+
62
+ # @see #dir
63
+ # @return [Array]
64
+ def files(vid, opts = {})
65
+ bd = normalized_base_dir(opts, :handle_recurse => true)
66
+ return process_items(@catalog.unmatched_files(bd), vid, opts)
67
+ end
68
+
69
+ # The workhorse for the the validation methods.
70
+ #
71
+ # @!visibility private
72
+ def process_items(items, vid, opts = {})
73
+ # Make sure the user did not forget to pass the validation identifier.
74
+ if vid.class == Hash
75
+ msg = "Validation identifier should not be a hash: #{vid.inspect}"
76
+ raise ArgumentError, msg
77
+ end
78
+
79
+ # Get a Quantifier object.
80
+ quant = DirValidator::Quantity.new(opts[:n] || '1+')
81
+
82
+ # We are given a list of unmatched Items (either files or dirs).
83
+ # Here we filter the list to those matching the name-related criteria.
84
+ items = name_filtered(items, opts)
85
+
86
+ # And here we cap the N of items to be returned. For example, if the
87
+ # user asked for 1-3 Items and we found 5, we will return only 3.
88
+ items = quantity_limited(items, quant)
89
+
90
+ # Add a warning if the N of Items is less than the user's expectation.
91
+ sz = items.size
92
+ unless sz >= quant.min_n
93
+ add_warning(vid, opts.merge(:got => sz))
94
+ end
95
+
96
+ # Mark the Items as matched so subsequent validations won't return same Items.
97
+ @catalog.mark_as_matched(items)
98
+
99
+ return items
100
+ end
101
+
102
+
103
+ ####
104
+ # Name-related filtering.
105
+ ####
106
+
107
+ # Takes an array of items and a validation-method opts hash.
108
+ # Returns the subset of those items matching the name-related
109
+ # criteria in the opts hash.
110
+ #
111
+ # @!visibility private
112
+ def name_filtered(items, opts)
113
+ # Filter the items to those in the base_dir.
114
+ # If there is no base_dir, no filtering occurs.
115
+ base_dir = normalized_base_dir(opts, :add_file_sep => true)
116
+ sz = base_dir.size
117
+ items = items.select { |i| i.path.start_with?(base_dir) } if sz > 0
118
+
119
+ # Set the item.target values, which are the values that will
120
+ # be subjected to the name-related test. If there is no base_dir,
121
+ # the target is the same as item.path.
122
+ items.each { |i| i.set_target(i.path[sz .. -1]) }
123
+
124
+ # Filter items to immediate children, unless user wants to recurse.
125
+ items = items.reject { |i| i.target.include?(FILE_SEP) } unless opts[:recurse]
126
+
127
+ # Return the items having targets matching the name regex.
128
+ rgx = name_regex(opts)
129
+ return items.select { |i| i.target_match(rgx) }
130
+ end
131
+
132
+ # Takes a validation-method opts hash.
133
+ # Returns opts[:base_dir] in a normalized form (with trailing separator).
134
+ # Returns nil or '' under certain conditions.
135
+ #
136
+ # @!visibility private
137
+ def normalized_base_dir(opts, nbd_opts = {})
138
+ return nil if opts[:recurse] and nbd_opts[:handle_recurse]
139
+ bd = opts[:base_dir]
140
+ return '' unless bd
141
+ bd.chop! while bd.end_with?(FILE_SEP)
142
+ bd += FILE_SEP if nbd_opts[:add_file_sep]
143
+ return bd
144
+ end
145
+
146
+ # Takes a validation-method opts hash.
147
+ # Returns the appropriate Regexp based on the name-related criteria.
148
+ #
149
+ # @!visibility private
150
+ def name_regex(opts)
151
+ name = opts[:name]
152
+ re = opts[:re]
153
+ pattern = opts[:pattern]
154
+ return Regexp.new(
155
+ name ? name_to_re(name) :
156
+ pattern ? pattern_to_re(pattern) :
157
+ re ? re : ''
158
+ )
159
+ end
160
+
161
+ # Takes a validation-method opts[:name] value.
162
+ # Returns the corresponding regex-ready string:
163
+ # - wrapped in start- and end- anchors
164
+ # - all special characters quoted
165
+ #
166
+ # @!visibility private
167
+ def name_to_re(name)
168
+ return az_wrap(Regexp.quote(name))
169
+ end
170
+
171
+ # Takes a validation-method opts[:pattern] value.
172
+ # Returns the corresponding regex-ready string
173
+ # - wrapped in start- and end- anchors
174
+ # - all special regex chacters quoted except for:
175
+ # * becomes .*
176
+ # ? becomes .
177
+ #
178
+ # @!visibility private
179
+ def pattern_to_re(pattern)
180
+ return az_wrap(Regexp.quote(pattern).gsub(/\\\*/, '.*').gsub(/\\\?/, '.'))
181
+ end
182
+
183
+ # Takes a string.
184
+ # Returns a new string wrapped in Regexp start-of-string and end-of-string anchors.
185
+ #
186
+ # @!visibility private
187
+ def az_wrap(s)
188
+ return "\\A#{s}\\z"
189
+ end
190
+
191
+
192
+ ####
193
+ # Quantity filtering.
194
+ ####
195
+
196
+ # Takes an array of items and a DirValidator::Quantity object.
197
+ # Returns the subset of those items falling within the max allowed
198
+ # by the Quantity.
199
+ #
200
+ # @!visibility private
201
+ def quantity_limited(items, quant)
202
+ return items[0 .. quant.max_index]
203
+ end
204
+
205
+
206
+ ####
207
+ # Methods related validation warnings and reporting.
208
+ ####
209
+
210
+ # Takes a validation identifier and a validation-method opts hash.
211
+ # Creates and new DirValidator::Warning and adds it to the validator's
212
+ # array of warnings.
213
+ #
214
+ # @!visibility private
215
+ def add_warning(vid, opts)
216
+ @warnings << DirValidator::Warning.new(vid, opts)
217
+ end
218
+
219
+ # Adds a warning to the validator for all unmatched items in the catalog.
220
+ # Should be run after all validation-methods have been called, typically
221
+ # before producing a report.
222
+ #
223
+ # @!visibility private
224
+ def validate
225
+ return if @validated # Run only once.
226
+ @catalog.unmatched_items.each do |item|
227
+ add_warning(EXTRA_VID, :path => item.path)
228
+ end
229
+ @validated = true
230
+ end
231
+
232
+ # Write a CSV report of the information contained in the validator's warnings.
233
+ #
234
+ # @param io [IO] IO object to which the report should be written.
235
+ def report(io = STDOUT)
236
+ require 'csv'
237
+ validate()
238
+ report_data.each do |row|
239
+ io.puts CSV.generate_line(row)
240
+ end
241
+ end
242
+
243
+ # Returns a matrix of warning information.
244
+ # Used when producing the CSV report.
245
+ #
246
+ # @!visibility private
247
+ def report_data
248
+ rc = DirValidator::Validator.report_columns
249
+ data = [rc]
250
+ @warnings.each do |w|
251
+ cells = rc.map { |c| v = w.opts[c]; v.nil? ? '' : v }
252
+ cells[0] = w.vid
253
+ data.push(cells)
254
+ end
255
+ return data
256
+ end
257
+
258
+ # Column headings for the CSV report.
259
+ #
260
+ # @!visibility private
261
+ def self.report_columns
262
+ return [:vid, :got, :n, :base_dir, :name, :re, :pattern, :path]
263
+ end
264
+
265
+ end
@@ -0,0 +1,28 @@
1
+ # @!attribute [r] vid
2
+ # @see #initialize
3
+ # @return [String]
4
+ # @!attribute [r] opts
5
+ # @see #initialize
6
+ # @return [Hash]
7
+ class DirValidator::Warning
8
+
9
+ attr_reader(:vid, :opts)
10
+
11
+ # @param vid [String] Validation identifier of the validation-method call
12
+ # that led to the warning.
13
+ # @param opts [Hash] Validation-method options hash, along with some additional
14
+ # information about the context in which the warning was
15
+ # generated.
16
+ def initialize(vid, opts)
17
+ @vid = vid
18
+ @opts = opts
19
+ end
20
+
21
+ # Returns a basic representation of the information contained in the warning.
22
+ #
23
+ # @return [String]
24
+ def to_s
25
+ return "#{@vid}: #{@opts.inspect}"
26
+ end
27
+
28
+ end
@@ -0,0 +1,17 @@
1
+ module DirValidator
2
+
3
+ # Syntactic sugar: a module method for creating a new validator.
4
+ #
5
+ # @param (see DirValidator::Validator#new)
6
+ # @return [DirValidator::Validator]
7
+ def self.new(*args, &block)
8
+ return DirValidator::Validator.new(*args, &block)
9
+ end
10
+
11
+ end
12
+
13
+ require 'dir_validator/catalog'
14
+ require 'dir_validator/item'
15
+ require 'dir_validator/quantity'
16
+ require 'dir_validator/validator'
17
+ require 'dir_validator/warning'
@@ -0,0 +1,88 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'dir_validator'
5
+
6
+ # Suppose that we want to check the following directory structure.
7
+ #
8
+ # spec/fixtures/tutorial/
9
+ # aaa/ # Top-level sub-directories should be 3 lower-case leters.
10
+ # 00/ # Should be three second-level directories: 00, 01, and 02.
11
+ # a.tif # One or more .tif files in the 00 directory.
12
+ # b.tif
13
+ # 01/
14
+ # a.jpg # A parallel set of .jpg files.
15
+ # b.jpg
16
+ # 02/
17
+ # a.jp2 # A parallel set of .jp2 files.
18
+ # b.jp2
19
+ # blort.txt # What?!?
20
+ # aab/
21
+ # 00/
22
+ # a.tif
23
+ # b.tif
24
+ # 01/
25
+ # a.jpg
26
+ # b.jpg
27
+ # 02/
28
+ # a.jp2 # Missing file.
29
+ # baa/
30
+ # 00/
31
+ # a.tif
32
+ # b.tif
33
+ # 01/
34
+ # a.jpg
35
+ # b.jpg
36
+ # 02/
37
+ # a.jp2 # Missing file.
38
+
39
+ # Set up the validator, passing in the starting path.
40
+ dv = DirValidator.new('tutorial/files')
41
+
42
+ # Here we set up our expectation for the top-level sub-directories. In this
43
+ # case, we are looking for 1 or more sub-directories named with exactly 3
44
+ # lower-case letters, as defined in the regular expression.
45
+ dv.dirs('top-level subdir', :re => /^[a-z]{3}$/).each do |subdir|
46
+
47
+ # Within each of those top-level directories, we expect to find three
48
+ # numbered directories. In this case, we pass in literal names to
49
+ # the validation methods rather than a regular expression.
50
+ # Notice also that the validation method is being called on the
51
+ # subdirectory (subdir), not the overall dir-validator (dv).
52
+ d0 = subdir.dir('00', :name => '00')
53
+ d1 = subdir.dir('01', :name => '01')
54
+ d2 = subdir.dir('02', :name => '02')
55
+
56
+ # In the 00 subdirectory, we expect to see a bunch of .tif files.
57
+ # We could have used a regular expression, but in this case we'll
58
+ # resort to a simple glob-like pattern.
59
+ d0.files('tifs', :pattern => '*.tif').each do |tif|
60
+
61
+ # And finally, we set up the validations for the parallel .jpg
62
+ # and .jp2 files. Those files should reside in the 01 and 02
63
+ # subdirectories (which we stored in d1 and d2 above). Their
64
+ # file names should mirror that of the current .tif file.
65
+ tif_base = tif.basename('.tif')
66
+ d1.file('jpgs', :name => tif_base + '.jpg')
67
+ d2.file('jp2s', :name => tif_base + '.jp2')
68
+
69
+ end
70
+ end
71
+
72
+ # We can generate a basic CSV report that will list:
73
+ # - items that we not found
74
+ # - extra items
75
+ #
76
+ # The CSV output (with extra spacing here for readability):
77
+ # vid, got, n, base_dir, name, re, pattern, path
78
+ # jp2s, 0, 1, aab/02, b.jp2, "", "", ""
79
+ # jp2s, 0, 1, baa/02, b.jp2, "", "", ""
80
+ # _EXTRA_, "", "", "", "", "", "", aaa/02/blort.txt
81
+ dv.report()
82
+
83
+ # Alternatively, we could examine the results programmatically by
84
+ # iterating over the dir-validator's warnings.
85
+ dv.validate()
86
+ dv.warnings.each do |w|
87
+ # ...
88
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dir_validator
3
+ version: !ruby/object:Gem::Version
4
+ hash: 47
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 12
9
+ - 0
10
+ version: 0.12.0
11
+ platform: ruby
12
+ authors:
13
+ - Monty Hindman
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-07-30 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 15
29
+ segments:
30
+ - 2
31
+ - 6
32
+ version: "2.6"
33
+ type: :development
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: lyberteam-devel
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :development
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: yard
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :development
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: awesome_print
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ type: :development
76
+ version_requirements: *id004
77
+ - !ruby/object:Gem::Dependency
78
+ name: looksee
79
+ prerelease: false
80
+ requirement: &id005 !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ hash: 3
86
+ segments:
87
+ - 0
88
+ version: "0"
89
+ type: :development
90
+ version_requirements: *id005
91
+ - !ruby/object:Gem::Dependency
92
+ name: rcov
93
+ prerelease: false
94
+ requirement: &id006 !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ hash: 3
100
+ segments:
101
+ - 0
102
+ version: "0"
103
+ type: :development
104
+ version_requirements: *id006
105
+ description: This gem provides a convenient syntax for checking whether the contents of a directory structure match your expectations.
106
+ email:
107
+ - hindman@stanford.edu
108
+ executables: []
109
+
110
+ extensions: []
111
+
112
+ extra_rdoc_files: []
113
+
114
+ files:
115
+ - lib/dir_validator/catalog.rb
116
+ - lib/dir_validator/item.rb
117
+ - lib/dir_validator/quantity.rb
118
+ - lib/dir_validator/validator.rb
119
+ - lib/dir_validator/warning.rb
120
+ - lib/dir_validator.rb
121
+ - LICENSE.rdoc
122
+ - README.rdoc
123
+ - CHANGELOG.rdoc
124
+ - tutorial/tutorial.rb
125
+ homepage: https://github.com/sul-dlss
126
+ licenses: []
127
+
128
+ post_install_message:
129
+ rdoc_options: []
130
+
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ none: false
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ hash: 3
139
+ segments:
140
+ - 0
141
+ version: "0"
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ none: false
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ hash: 3
148
+ segments:
149
+ - 0
150
+ version: "0"
151
+ requirements: []
152
+
153
+ rubyforge_project:
154
+ rubygems_version: 1.8.24
155
+ signing_key:
156
+ specification_version: 3
157
+ summary: Validate content of a directory structure.
158
+ test_files: []
159
+
160
+ has_rdoc: