Class: FrontmatterTests

Inherits:
Jekyll::Command
  • Object
show all
Defined in:
lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_config.rb,
lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_helper.rb,
lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_loader.rb,
lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_tester.rb,
lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_processor.rb,
lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_validator.rb,
lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_initializer.rb

Class Method Summary collapse

Class Method Details

.basepathObject



30
31
32
# File 'lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_validator.rb', line 30

def basepath
  File.join(Dir.pwd, 'tests', 'schema')
end

.check_keys(target, keys, title) ⇒ Object

Public: checks a hash for expected keys

target - the hash under test keys - an array of keys the data is expected to have, usually loaded from

a schema file by loadschema()

title - A string representing ‘data`’s name



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_validator.rb', line 12

def check_keys(target, keys, title)
  keys -= ['config']
  unless target.respond_to?('keys')
    puts "The file #{title} is missing all frontmatter.".red
    return false
  end
  diff = keys - target.keys
  if diff.empty?
    return true
  else
    puts "\nThe file #{title} is missing the following keys:".red
    for k in diff
      puts "    * #{k}".red
    end
    return false
  end
end

.check_one_of(data, key, value) ⇒ Object



75
76
77
78
79
80
81
82
83
# File 'lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_validator.rb', line 75

def check_one_of(data, key, value)
  if value.keys.include?('one_of') && !one_of?(data[key], value['one_of'])
    puts "    * One of error: One of '#{data[key]}' was not".red
    puts "                    in the list of expected values in".red
    puts "                    #{File.join(basepath, value['one_of'])}\n".yellow

    false
  end
end

.check_rules(data, key, value) ⇒ Object



85
86
87
88
89
90
91
92
# File 'lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_validator.rb', line 85

def check_rules(data, key, value)
  if value.keys.include?('rules') && !follows_rules?(data[key], value['rules'])
    puts "    * Rules error: One of '#{data[key]}'".red
    puts "                   doesn't follow the rules defined in".red
    puts "                   #{basepath}/rules.yml\n".yellow
    false
  end
end

.check_types(data, schema, file) ⇒ Object

Internal: eventually, validate that the values match expected types

For example, if we expect the ‘date` key to be in yyyy-mm-dd format, validate that it’s been entered in that format. If we expect authors to be an array, make sure we’re getting an array.



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
# File 'lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_validator.rb', line 39

def check_types(data, schema, file)
  return false unless data.respond_to?('keys')
  schema.each do |s|
    key = s[0]
    value = s[1]
    type = if value.class == Hash
             value['type']
           else
             value
           end

    next unless required?(key, schema)
    if key == 'config'
      next
    elsif value.class == Hash
      next unless value.keys.include?('one_of') || value.keys.include?('rules')
      violate_one_of = check_one_of(data, key, value) == false
      violate_rules = check_rules(data, key, value) == false
      return false if violate_one_of || violate_rules

    elsif type == 'Array' && data[key].class == Array
      next
    elsif type == 'Boolean' && data[key].is_a?(Boolean)
      next
    elsif type == 'String' && data[key].class == String
      next
    elsif type == 'Date'
      next
    else
      puts "    * invalid value for '#{key}' in #{file}. " \
           "Expected #{type} but was #{data[key].class}\n\n"
      return false
    end
  end
end

.follows_rules?(value, rules) ⇒ Boolean

Returns:



19
20
21
22
23
24
25
26
27
# File 'lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_helper.rb', line 19

def follows_rules?(value, rules)
  if rules.include?('no-dash') && rules.include?('lowercase')
    FrontmatterRules.dashless?(value) && FrontmatterRules.lowercase?(value)
  elsif rules.include?('no-dash') && !rules.include?('lowercase')
    FrontmatterRules.dashless?(value)
  elsif !rules.include?('no-dash') && rules.include?('lowercase')
    FrontmatterRules.lowercase?(value)
  end
end

.init_with_program(prog) ⇒ Object

Internal: fired when ‘jekyll test` is run.

When ‘jekyll test` runs, `test_frontmatter` is fired with options and args passed from the command line.



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_initializer.rb', line 11

def init_with_program(prog)
  prog.command(:test) do |c|
    c.syntax 'test [options]'
    c.description 'Test your site for frontmatter.'

    c.option 'posts', '-p', 'Target only posts'
    c.option 'collections', '-c [COLLECTION]', 'Target a specific collection'
    c.option 'all', '-a', 'Test all collections (Default)'

    c.action do |args, options|
      options = { 'all' => true } if options.empty?
      test_frontmatter(args, options)
    end
  end
end

.load_schema(file) ⇒ Object

Public: Load a schema from file.

file - a string containing a filename

Used throughout to load a specific file. In the future the directories where these schema files are located could be loaded from _config.yml

Returns a hash loaded from the YAML doc or exits 1 if no schema file exists.



15
16
17
18
19
20
21
22
23
24
25
# File 'lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_loader.rb', line 15

def load_schema(file)
  # binding.pry
  schema = File.join(Dir.pwd, schema_config['path'], file)
  # binding.pry
  if File.exist?(schema)
    YAML.load_file(schema)
  else
    puts "No schema for #{file}"
    exit 1
  end
end

.one_of?(data, schema) ⇒ Boolean

Returns:



6
7
8
9
10
11
12
13
14
15
16
17
# File 'lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_helper.rb', line 6

def one_of?(data, schema)
  if schema.instance_of?(Array) && data.instance_of?(Array)
    (schema & data).count == data.count
  elsif schema.include? '.yml'
    schema_list = YAML.load_file(File.join(Dir.pwd, 'tests', 'schema', schema))
    (schema_list & data).count == data.count
  elsif schema.instance_of?(String) && data.instance_of?(Array)
    false
  else
    schema == data
  end
end

.process(schema) ⇒ Object

Public: processes a collection against a schema

schema - the hash-representation of a schema file

Opens each file in the collection’s expected directory and checks the file’s frontmatter for the expected keys and the expected format of the values.

NOTE - As it iterates through files, subdirectories will be ignored

Returns true or false depending on the success of the check.



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_processor.rb', line 17

def process(schema)
  dir = File.join(schema['config']['path'])
  passfail = []
  Dir.open(dir).each do |f|
    next if File.directory?(File.join(dir, f))
    file = File.open(File.join(dir, f))
    next if schema['config']['ignore'].include?(f)
    data = YAML.load_file(file)

    passfail.push check_keys(data, schema.keys, f)
    passfail.push check_types(data, schema, File.join(dir, f))
  end
  passfail.keep_if { |p| p == false }
  if passfail.empty?
    return true
  else
    puts "There were #{passfail.count} errors".red
    return false
  end
end

.required?(key, schema) ⇒ Boolean

Returns:



29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_helper.rb', line 29

def required?(key, schema)
  is_required = true
  is_primary = schema[key]
  schema['config'] = schema['config'] || { 'optional': [] }
  is_optional = schema['config']['optional'].include?(key)

  if is_primary && !is_optional
    is_required
  elsif (is_primary && is_optional) || (!is_primary && is_optional)
    !is_required
  else
    raise 'The key provided is not in the schema.'
  end
end

.schema_configObject



5
6
7
8
9
10
11
# File 'lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_config.rb', line 5

def schema_config
  config = Jekyll.configuration
  unless config.key?('frontmatter_tests')
    config['frontmatter_tests'] = { 'path' => File.join('deploy', 'tests', 'schema') }
  end
  config['frontmatter_tests']
end

.test_collections(collections) ⇒ Object

Public: Tests only specific collection documents

collections - a comma separated string of collection names.

‘collections` is split into an array and each document is loaded and processed against its respective schema.



55
56
57
58
59
60
61
62
63
# File 'lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_tester.rb', line 55

def test_collections(collections)
  yepnope = []
  for c in collections
    puts "Testing #{c}".green
    yepnope.push process(load_schema("_#{c}.yml"))
    puts "Finished testing #{c}".green
  end
  yepnope
end

.test_everythingObject

Public: Tests all collections described by a schema file at ‘deploy/tests/schema`



67
68
69
70
71
72
73
74
75
76
77
# File 'lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_tester.rb', line 67

def test_everything
  schema = Dir.open(schema_config['path'])
  yepnope = []
  schema.each do |s|
    next unless s.start_with?('_')
    puts "Testing #{s}".green
    yepnope.push process(load_schema(s))
    puts "Finished testing #{s}".green
  end
  yepnope
end

.test_frontmatter(_args, options) ⇒ Object

Public: Processes options passed throguh the command line, runs the appropriate tests.

args - command line arguments (example: jekyll test [ARG]) options - command line options (example: jekyll test -[option] [value])

Depending on the flag passed (see ‘init_with_program`), runs the expected # test.

Example: the following comamnd ‘jekyll test -p` will pass “=>

true` as `options`. This will cause `test_frontmatter` to
compare all docs in _posts with the provided schema.

The test runner pushes the result of each test into a ‘results` array and # exits `1` if any tests fail or `0` if all is well.



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_tester.rb', line 19

def test_frontmatter(_args, options)
  puts 'starting tests'
  if options['posts']
    results = test_posts
  elsif options['collections']
    collections = options['collections'].split(',')
    results = test_collections(collections)
  else
    results = test_everything
  end
  if results.find_index { |r| r == false }
    puts 'The test exited with errors, see above.'
    exit 1
  else
    puts 'Tests finished!'
    exit 0
  end
end

.test_postsObject

Public: tests all documents that are “posts”

Loads a schema called _posts.yml and processes all post documents against it.



42
43
44
45
46
47
# File 'lib/jekyll_frontmatter_tests/jekyll_frontmatter_tests_tester.rb', line 42

def test_posts
  puts 'testing posts'.green
  yepnope = [].push process(load_schema('_posts.yml'))
  puts 'Finished testing'.green
  yepnope
end