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

.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_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.



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

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
      if value.keys.include? 'one_of'
        if !one_of?(data[key], value['one_of'])
          puts "    * '#{data[key]}' was not in the list " \
               "of expected values in #{file}.".red
          puts "      expected one of the following: #{s[1]['one_of']}\n".red
          return false
        else
          next
        end
      else
        next
      end
    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 "    * '#{key}' is not a valid key in #{file}. " \
           "Expected #{type} but was #{data[key].class}\n\n"
      return false
    end
  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:



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

def required?(key, schema)
  if schema['config']
    !schema['config']['optional'].include? key
  else
    true
  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