module SchemaValidator
Validates the frontmatter of all files
Public Class Methods
lint_faq_file(fn)
click to toggle source
# File bin/validate-frontmatter.rb, line 130 def self.lint_faq_file(fn) errs = [] data = lintable?(fn) return data if data.nil? || data.is_a?(Array) errs.push(*validate_document(data, @faq_validator)) errs end
lint_material(fn)
click to toggle source
# File bin/validate-frontmatter.rb, line 148 def self.lint_material(fn) # Any error messages errs = [] data = lintable?(fn) return data if data.nil? || data.is_a?(Array) # Load topic metadata for this file topic = fn.split('/')[2] topic_metadata = YAML.load_file("topics/#{topic}/metadata.yaml") # Load subtopic titles if data.key?('subtopic') subtopic_ids = [] topic_metadata['subtopics'].each do |x| subtopic_ids.push(x['id']) end @TUTORIAL_SCHEMA['mapping']['subtopic']['enum'] = subtopic_ids @SLIDES_SCHEMA['mapping']['subtopic']['enum'] = subtopic_ids @tutorial_validator = Kwalify::Validator.new(@TUTORIAL_SCHEMA) @slides_validator = Kwalify::Validator.new(@SLIDES_SCHEMA) end # Generic error handling: ## Check requirements errs.push(*validate_requirements(data['requirements'])) if data.key?('requirements') ## Check follow ups errs.push(*validate_requirements(data['follow_up_training'])) if data.key?('follow_up_training') # Custom error handling: if tutorial?(fn) errs.push(*validate_document(data, @tutorial_validator)) elsif slide?(fn) errs.push(*validate_document(data, @slides_validator)) end # Check contributors OR contributions if (slide?(fn) || tutorial?(fn)) && !(data.key?('contributors') || data.key?('contributions')) errs.push('Document lacks EITHER contributors OR contributions key') end # If we had no errors, validated successfully errs end
lint_news_file(fn)
click to toggle source
# File bin/validate-frontmatter.rb, line 194 def self.lint_news_file(fn) errs = [] data = lintable?(fn) return data if data.nil? || data.is_a?(Array) if data.key?('cover') if !data['cover'].start_with?('https://') if !File.exist?(data['cover']) errs.push("Cover image #{data['cover']} does not exist") end end end errs.push(*validate_document(data, @news_validator)) errs end
lint_quiz_file(fn)
click to toggle source
# File bin/validate-frontmatter.rb, line 211 def self.lint_quiz_file(fn) errs = [] data = lintable?(fn) return data if data.nil? || data.is_a?(Array) data['questions'].select { |q| q.key? 'correct' }.each do |q| if q['correct'].is_a?(Array) if q['type'] != 'choose-many' errs.push("There are multiple answers for this question, but it is not a choose-many #{q['title']}") end q['correct'].each do |c| errs.push("Answer #{c} not included in options for question #{q['title']}") if !q['answers'].include?(c) end else if q['type'] != 'choose-1' errs.push("There is only a single textual answer, it must be a list for a choose-many question #{q['title']}") end if !q['answers'].include?(q['correct']) errs.push("Answer #{q['correct']} not included in options for question #{q['title']}") end end end errs.push(*validate_document(data, @quiz_validator)) errs end
lint_topic(fn)
click to toggle source
# File bin/validate-frontmatter.rb, line 139 def self.lint_topic(fn) # Any error messages errs = [] data = lintable?(fn) return data if data.nil? || data.is_a?(Array) errs.push(*validate_document(data, @topic_validator)) end
lintable?(fn)
click to toggle source
# File bin/validate-frontmatter.rb, line 110 def self.lintable?(fn) begin begin data = YAML.load_file(fn, permitted_classes: [Date]) rescue StandardError data = YAML.load_file(fn) end rescue StandardError => e return ["YAML error, failed to parse #{fn}, #{e}"] end # Check this is something we actually want to process if !data.is_a?(Hash) puts "Skipping #{fn}" return nil end data end
run()
click to toggle source
# File bin/validate-frontmatter.rb, line 240 def self.run errors = [] # Topics materials = (Dir.glob('./metadata/*.yaml') + Dir.glob('./metadata/*.yml')) .grep_v(/schema-*/) .select do |x| d = YAML.load_file(x) # Ignore non-hashes d.is_a?(Hash) && (d.key? 'editorial_board' or d.key? 'summary' or d.key? 'type') end errors += materials.map { |x| [x, lint_topic(x)] } # Lint tutorials/slides/metadata materials = Dir.glob('./topics/**/slides.*html') + Dir.glob('./topics/**/tutorial.*md') errors += materials.map { |x| [x, lint_material(x)] } # Lint FAQs errors += Dir.glob('**/faqs/**/*.md') .grep_v(/aaaa_dontquestionthislinkitisthegluethatholdstogetherthegalaxy/) .grep_v(/index.md$/) .grep_v(/README.md$/) .map { |x| [x, lint_faq_file(x)] } # Lint quizzes errors += Dir.glob('./topics/**/quiz/*') .grep(/ya?ml$/) .map { |x| [x, lint_quiz_file(x)] } # Lint news errors += Dir.glob('./news/_posts/*') .map { |x| [x, lint_news_file(x)] } errors.reject! { |_path, errs| errs.nil? or errs.empty? } errors end
slide?(fn)
click to toggle source
# File bin/validate-frontmatter.rb, line 66 def self.slide?(fn) fn.include?('slides.html') || fn =~ /slides_[A-Z]{2,}.html/ end
tutorial?(fn)
click to toggle source
# File bin/validate-frontmatter.rb, line 62 def self.tutorial?(fn) fn.include?('tutorial.md') || fn =~ /tutorial_[A-Z]{2,}.md/ end
validate_document(document, validator)
click to toggle source
# File bin/validate-frontmatter.rb, line 48 def self.validate_document(document, validator) errors = validator.validate(document) return errors if errors && !errors.empty? [] end
validate_non_empty_key_value(map, key)
click to toggle source
# File bin/validate-frontmatter.rb, line 55 def self.validate_non_empty_key_value(map, key) return ["Missing #{key} for requirement"] unless map.key?(key) return ["Empty #{key} for requirement"] if map[key].empty? [] end
validate_requirements(requirements)
click to toggle source
# File bin/validate-frontmatter.rb, line 70 def self.validate_requirements(requirements) errs = [] # Exit early if no requirements return [] if requirements.nil? || requirements.empty? # Otherwise check each requirements.each do |requirement| # For external links, they need a link that is non-empty case requirement['type'] when 'external' errs.push(*validate_document(requirement, @requirement_external_validator)) when 'internal' errs.push(*validate_document(requirement, @requirement_internal_validator)) # For the internal requirements, test that they point at something real. if requirement.key?('tutorials') requirement['tutorials'].each do |tutorial| # For each listed tutorial check that a directory with that name exists pn = Pathname.new("topics/#{requirement['topic_name']}/tutorials/#{tutorial}") if !pn.directory? errs.push("Internal requirement to topics/#{requirement['topic_name']}/tutorials/#{tutorial} " \ 'does not exist') end end end when 'none' errs.push(*validate_non_empty_key_value(requirement, 'title')) requirement.each_key do |x| errs.push("Unknown key #{x}") if !%w[title type].include?(x) end else errs.push("Unknown requirement type #{requirement['type']}") end end errs end