I have a requirement to validate file names related to architecture after they are uploaded. Once they have been uploaded I must warn the user if the file name is not standards compliant.
What's in a name
To be standards compliant a file name must consist of 7 parts after the extension is removed from the name, and:
- part 1 is the project code; an arbitrary set of letters (including diacritics, Ã, Â, etc) and numbers.
- part 2 is the discipline that the file relates to.
- part 3 is the project phase.
- part 4 is a 4-digit document number in the format of xxxx (0001, 0002, etc...)
- part 5 is the subject that the document relates to.
- part 6 is the floor that the project relates to.
- part 7 is the revision number; the format is RXX (R00, R01, etc...)
- parts must be in said order.
Parts 2, 3, 5, and 6 must each be an abbreviation in a predefined set of values. Validating them is a simple matter of looking up if the abbreviate exists.
I wrote a single class for each part. For the sake of brevity I included only one class out of four. But assume all four are identical. The only difference is the constant of acceptable abbreviations.
class FileName
attr_reader :name
def self.valid?(name)
new(name).valid?
end
# Valid file name
# ABCD-ARQ-AP-0022-ACS-LOC-R00.jpeg
def initialize(name)
# Split individual parts into an array, ignoring .extension
@name = name.split('.').first.split('-')
end
def valid?
name.length == 7 &&
project_code_valid? &&
discipline_valid? &&
phase_valid? &&
document_number_valid? &&
subject_valid? &&
level_valid? &&
revision_valid?
end
def project_code
@project_code ||= name[0]
end
def project_code_valid?
project_code !~ /\P{Alnum}/ && project_code.length == 4
end
def discipline
@discipline ||= name[1]
end
def discipline_valid?
Discipline.value_valid?(discipline)
end
def phase
@phase ||= name[2]
end
def phase_valid?
Phase.value_valid?(phase)
end
def document_number
@document_number ||= name[3]
end
def document_number_valid?
document_number !~ /[^0-9]/ && document_number.length == 4
end
def subject
@subject ||= name[4]
end
def subject_valid?
Subject.value_valid?(subject)
end
def level
@level ||= name[5]
end
def level_valid?
Level.value_valid?(level)
end
def revision
@revision ||= name[6].split('R')
end
def revision_valid?
revision.length == 2 &&
revision.first.empty? &&
revision.last !~ /[^0-9]/ &&
revision.last.length == 2
end
end
class Discipline
DISCIPLINES = {
'Acessibilidade' => 'ACE',
'Acústica' => 'ACU'
# Snip...
}
def self.value_valid?(value)
new(value).value_valid?
end
def initialize(value)
@value = value
end
def value_valid?
DISCIPLINES.values.include?(@value)
end
end
class Phase
# Snip...
end
class Subject
# Snip...
end
class Level
# Snip...
end
Usage
# files_helper.rb
def file_name_valid?(file)
unless FileName.valid?(file.name)
content_tag :span, 'Invalid File Name', class: 'label-danger'
end
end
Issues I have with the code
- The aforementioned classes for parts 2, 3, 5, and 6 have a lot of duplication. As mentioned, they are identical except for the hash of acceptable values. Maybe inheritance is a good option?
- The
valid?
method looks very long winded. - The classes are floating around in the file, raising the possibility of a name clash.
- The
FileName
class should be instantiated once. It should be a singleton. I'm not sure how to implement singletons in Rails. - There's a lot of memoization (
@level ||= name[6]
) in the accessor methods I wrote for the different name parts. - Can the
project_code_valid?
anddocument_number_valid?
methods be more compact? Can Regex be used to check string length instead of having a separate check? - All abbreviations (parts 2, 3, 5, and 6) should be uppercase; my regexes do not account for this.
- Again in parts parts 2, 3, 5, and 6, I am instantiating the class each time. Should this be memoized?
- The
revision
method seems too magical.- It splits the string by
R
. A valid rev should produce an array of two elements:["", "00"]
. - The first element of this array has to be
empty?
because splitting byR
removes it and returns an empty string. - And the last element has to be a number made up of two digits. I'm not sure this will be readable in 5 months.
- It splits the string by
- The same can be said about the readability of my accessor methods that retrieve the parts. E.g.
@level ||= name[5]
. There are important details hidden inname[5]
. And that is the 5th part should represent a level. This does not seem obvious enough.